truffler 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +71 -8
- data/lib/generators/truffler/install/templates/migration.rb.tt +6 -3
- data/lib/generators/truffler/upgrade/templates/backfill_spends_tenant_key_migration.rb.tt +25 -0
- data/lib/generators/truffler/upgrade/templates/labels_search_covering_migration.rb.tt +32 -0
- data/lib/generators/truffler/upgrade/upgrade_generator.rb +49 -3
- data/lib/tasks/truffler.rake +28 -13
- data/lib/truffler/benchmark/runner.rb +1 -1
- data/lib/truffler/clients/evaluator.rb +44 -0
- data/lib/truffler/configuration.rb +9 -0
- data/lib/truffler/definition.rb +52 -0
- data/lib/truffler/embeddings/backfill.rb +40 -11
- data/lib/truffler/embeddings/label_vector.rb +1 -1
- data/lib/truffler/embeddings/neighbor_store.rb +33 -7
- data/lib/truffler/embeddings/vector_store.rb +9 -3
- data/lib/truffler/jobs/backfill_job.rb +14 -5
- data/lib/truffler/jobs/embed_job.rb +2 -0
- data/lib/truffler/jobs/label_flush_job.rb +4 -2
- data/lib/truffler/jobs/resume_job.rb +22 -9
- data/lib/truffler/label_definition.rb +13 -2
- data/lib/truffler/labeling/backfill.rb +100 -37
- data/lib/truffler/labeling/labeler.rb +20 -5
- data/lib/truffler/labeling/queue.rb +14 -9
- data/lib/truffler/labeling/supplied.rb +16 -5
- data/lib/truffler/lenses/backfill.rb +28 -10
- data/lib/truffler/model.rb +5 -0
- data/lib/truffler/providers/backup.rb +1 -1
- data/lib/truffler/query_encoding/encoder.rb +10 -6
- data/lib/truffler/records/backfill_spend.rb +30 -6
- data/lib/truffler/redaction.rb +1 -1
- data/lib/truffler/search/encoding.rb +12 -6
- data/lib/truffler/search/filler.rb +31 -6
- data/lib/truffler/search/keystroke.rb +29 -15
- data/lib/truffler/search/result.rb +11 -1
- data/lib/truffler/search/sql.rb +70 -10
- data/lib/truffler/smart_search/dispatcher.rb +1 -1
- data/lib/truffler/smart_search/starter.rb +1 -1
- data/lib/truffler/version.rb +1 -1
- metadata +4 -1
data/lib/truffler/search/sql.rb
CHANGED
|
@@ -3,15 +3,24 @@ module Truffler
|
|
|
3
3
|
# Builds the one keystroke query of KTD8, scored per KTD20:
|
|
4
4
|
#
|
|
5
5
|
# w_label * SUM(weight * value) over the intent's nonzero label keys
|
|
6
|
-
# + w_text * text similarity (inline SQL, or the store's top-K CASE)
|
|
6
|
+
# + w_text * text similarity (a top-K join, inline SQL, or the store's top-K CASE)
|
|
7
7
|
# + w_keyword * keyword hit + w_exact * exact-source hit
|
|
8
8
|
# + SOFT_KEYWORD * w_keyword * soft keyword hit
|
|
9
9
|
#
|
|
10
10
|
# A weighted dot product, not cosine: cosine would divide out magnitude
|
|
11
11
|
# and let a record high on unrelated labels outrank the one the query
|
|
12
12
|
# asked for. Hard filters are EXISTS subqueries that run before scoring.
|
|
13
|
+
#
|
|
14
|
+
# At scale: when every record past the filters is a candidate (label-only
|
|
15
|
+
# and filtered searches), label scores come from one grouped aggregate
|
|
16
|
+
# LEFT JOINed on record_id instead of a subquery per row. A store with
|
|
17
|
+
# `neighbors_sql` (NeighborStore on Postgres) is LEFT JOINed the same
|
|
18
|
+
# way, so text similarity is read from the tenant's top-K. Relation
|
|
19
|
+
# sources run once, as `id = ANY(ARRAY(subquery))` on Postgres.
|
|
13
20
|
class Sql
|
|
14
21
|
LABELS = "truffler_labels".freeze
|
|
22
|
+
LABEL_SCORES = "truffler_label_scores".freeze
|
|
23
|
+
NEIGHBORS = "truffler_neighbors".freeze
|
|
15
24
|
# Share of the keyword weight a soft keyword hit adds (see Encoding).
|
|
16
25
|
SOFT_KEYWORD = 0.25
|
|
17
26
|
|
|
@@ -38,7 +47,8 @@ module Truffler
|
|
|
38
47
|
# filter the filter decides membership and text matches only rank.
|
|
39
48
|
def candidates(scope)
|
|
40
49
|
base = base(scope)
|
|
41
|
-
|
|
50
|
+
base = base.joins(Arel.sql(neighbors_join_sql)) if neighbors_sql
|
|
51
|
+
return base if every_base_record?
|
|
42
52
|
|
|
43
53
|
conditions = [ keyword_sql, exact_sql, (text_candidate_sql if text_score_sql) ].compact
|
|
44
54
|
base.where(Arel.sql(conditions.any? ? conditions.map { |condition| "(#{condition})" }.join(" OR ") : "1 = 0"))
|
|
@@ -46,19 +56,21 @@ module Truffler
|
|
|
46
56
|
|
|
47
57
|
def relation(scope, limit: nil)
|
|
48
58
|
relation = candidates(scope)
|
|
59
|
+
relation = relation.joins(Arel.sql(label_scores_join_sql)) if grouped_label_scores?
|
|
49
60
|
relation = relation.select(Arel.sql("#{table}.*")) if relation.select_values.empty?
|
|
50
61
|
relation = relation.select(*score_columns.map { |name, sql| Arel.sql("(#{sql}) AS #{name}") })
|
|
51
62
|
relation = relation.reorder(*ordering)
|
|
52
63
|
limit ? relation.limit(limit) : relation
|
|
53
64
|
end
|
|
54
65
|
|
|
66
|
+
# The label term: a per-candidate subquery when sources narrow the
|
|
67
|
+
# candidates, else the grouped join's score.
|
|
55
68
|
def label_score_sql
|
|
56
|
-
|
|
57
|
-
return if
|
|
69
|
+
return if encoding.intent_vector.empty?
|
|
70
|
+
return "COALESCE(#{connection.quote_table_name(LABEL_SCORES)}.score, 0.0)" if grouped_label_scores?
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"WHERE #{label_scope_sql} AND #{label_column('label_key')} IN (#{intent.keys.map { |key| quote(key) }.join(', ')})), 0.0)"
|
|
72
|
+
"COALESCE((SELECT SUM(#{label_case_sql}) FROM #{quoted_labels} " \
|
|
73
|
+
"WHERE #{label_scope_sql} AND #{label_keys_sql}), 0.0)"
|
|
62
74
|
end
|
|
63
75
|
|
|
64
76
|
def sources
|
|
@@ -67,7 +79,7 @@ module Truffler
|
|
|
67
79
|
end
|
|
68
80
|
|
|
69
81
|
def keywords
|
|
70
|
-
encoding.keywords(query)
|
|
82
|
+
@keywords ||= encoding.keywords(query, keep: -> { Filler.label_words(definition, tenant_key) })
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
private
|
|
@@ -115,6 +127,46 @@ module Truffler
|
|
|
115
127
|
range.to ? scope.where(arrived_at.lt(range.to)) : scope
|
|
116
128
|
end
|
|
117
129
|
|
|
130
|
+
def label_case_sql
|
|
131
|
+
cases = encoding.intent_vector.map { |key, weight| "WHEN #{quote(key)} THEN #{Float(weight)} * #{label_column('value')}" }
|
|
132
|
+
"CASE #{label_column('label_key')} #{cases.join(' ')} ELSE 0.0 END"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def label_keys_sql
|
|
136
|
+
"#{label_column('label_key')} IN (#{encoding.intent_vector.keys.map { |key| quote(key) }.join(', ')})"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Every record past the tenant, time, and filters is a candidate.
|
|
140
|
+
def every_base_record?
|
|
141
|
+
label_only? || encoding.filters.any?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def grouped_label_scores?
|
|
145
|
+
encoding.intent_vector.any? && every_base_record?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# One aggregate over the tenant's rows for the intent's keys, which
|
|
149
|
+
# `index_truffler_labels_for_search` (with INCLUDE (record_id)) serves
|
|
150
|
+
# as an index-only scan.
|
|
151
|
+
def label_scores_join_sql
|
|
152
|
+
tenant = definition.scoped? ? " AND #{label_column('tenant_key')} = #{quote(tenant_key)}" : ""
|
|
153
|
+
scores = connection.quote_table_name(LABEL_SCORES)
|
|
154
|
+
"LEFT JOIN (SELECT #{label_column('record_id')} AS record_id, SUM(#{label_case_sql}) AS score FROM #{quoted_labels} " \
|
|
155
|
+
"WHERE #{label_column('record_type')} = #{quote(model.polymorphic_name)}#{tenant} AND #{label_keys_sql} " \
|
|
156
|
+
"GROUP BY #{label_column('record_id')}) #{scores} ON #{scores}.record_id = #{primary_key}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def neighbors_sql
|
|
160
|
+
return @neighbors_sql if defined?(@neighbors_sql)
|
|
161
|
+
|
|
162
|
+
@neighbors_sql = (store.neighbors_sql(model, tenant_key: tenant_key, vector: vector) if vector && store.respond_to?(:neighbors_sql))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def neighbors_join_sql
|
|
166
|
+
neighbors = connection.quote_table_name(NEIGHBORS)
|
|
167
|
+
"LEFT JOIN (#{neighbors_sql}) #{neighbors} ON #{neighbors}.record_id = #{primary_key}"
|
|
168
|
+
end
|
|
169
|
+
|
|
118
170
|
def label_filter_sql(key, threshold)
|
|
119
171
|
"EXISTS (SELECT 1 FROM #{quoted_labels} WHERE #{label_scope_sql} AND #{label_column('label_key')} = #{quote(key)} " \
|
|
120
172
|
"AND #{label_column('value')} >= #{Float(threshold)})"
|
|
@@ -171,7 +223,10 @@ module Truffler
|
|
|
171
223
|
def text_score_sql
|
|
172
224
|
return @text_score_sql if defined?(@text_score_sql)
|
|
173
225
|
|
|
174
|
-
@text_score_sql =
|
|
226
|
+
@text_score_sql =
|
|
227
|
+
if neighbors_sql then "COALESCE(#{connection.quote_table_name(NEIGHBORS)}.similarity, 0.0)"
|
|
228
|
+
elsif vector && store then store.similarity_sql(model, tenant_key: tenant_key, vector: vector).to_s
|
|
229
|
+
end
|
|
175
230
|
end
|
|
176
231
|
|
|
177
232
|
def text_candidate_sql
|
|
@@ -186,9 +241,14 @@ module Truffler
|
|
|
186
241
|
definition.scoped? ? model.where(definition.tenant_column => tenant_key) : model.all
|
|
187
242
|
end
|
|
188
243
|
|
|
244
|
+
# An id list is the fast path. A relation runs once as an array on
|
|
245
|
+
# Postgres, where `IN (subquery)` becomes a hashed filter over every
|
|
246
|
+
# tenant row.
|
|
189
247
|
def membership_sql(result)
|
|
190
248
|
case result
|
|
191
|
-
when ActiveRecord::Relation
|
|
249
|
+
when ActiveRecord::Relation
|
|
250
|
+
subquery = result.reselect(result.klass.arel_table[result.klass.primary_key]).to_sql
|
|
251
|
+
connection.adapter_name.match?(/postg/i) ? "#{primary_key} = ANY(ARRAY(#{subquery}))" : "#{primary_key} IN (#{subquery})"
|
|
192
252
|
when nil then nil
|
|
193
253
|
else
|
|
194
254
|
ids = Array(result)
|
|
@@ -64,7 +64,7 @@ module Truffler
|
|
|
64
64
|
if @encodings.encoded?(key) then @encodings.read_encoding(key, query)
|
|
65
65
|
elsif @encodings.in_flight?(key) then @encoder.await(key, deadline: @deadline, query: query)
|
|
66
66
|
end
|
|
67
|
-
encoding&.without(run.suppressed)
|
|
67
|
+
encoding&.without(run.suppressed, keep_words: -> { Search::Filler.label_words(model.truffler_definition, run.tenant_key) })
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# The snapshot narrowed to what the encoding allows, in the tenant, in
|
|
@@ -30,7 +30,7 @@ module Truffler
|
|
|
30
30
|
local_ids = result.ids
|
|
31
31
|
run = Run.create(@model, query: @query.raw.strip, tenant_key: keystroke.tenant_key, user_key: keystroke.user_key,
|
|
32
32
|
surface: keystroke.surface, suppressed: @suppressed, pool_ids: pool(result, local_ids), local_ids: local_ids,
|
|
33
|
-
local_weak: result.
|
|
33
|
+
local_weak: result.local_weak?, explicit_action: result.explicit_action, store: @store)
|
|
34
34
|
previous = @store.supersede(run.record_type, run.tenant_key, run.user_key, run.surface, run.id)
|
|
35
35
|
Run.load(previous, store: @store).cancel! if previous
|
|
36
36
|
Instrumentation.instrument(:smart_search, run_id: run.id, record_type: run.record_type, tenant_key: run.tenant_key,
|
data/lib/truffler/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: truffler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kieran Klaassen
|
|
@@ -202,6 +202,8 @@ files:
|
|
|
202
202
|
- lib/generators/truffler/install/templates/initializer.rb.tt
|
|
203
203
|
- lib/generators/truffler/install/templates/migration.rb.tt
|
|
204
204
|
- lib/generators/truffler/upgrade/templates/backfill_spends_migration.rb.tt
|
|
205
|
+
- lib/generators/truffler/upgrade/templates/backfill_spends_tenant_key_migration.rb.tt
|
|
206
|
+
- lib/generators/truffler/upgrade/templates/labels_search_covering_migration.rb.tt
|
|
205
207
|
- lib/generators/truffler/upgrade/upgrade_generator.rb
|
|
206
208
|
- lib/tasks/truffler.rake
|
|
207
209
|
- lib/tasks/truffler/bench.rake
|
|
@@ -224,6 +226,7 @@ files:
|
|
|
224
226
|
- lib/truffler/clients/base.rb
|
|
225
227
|
- lib/truffler/clients/callable.rb
|
|
226
228
|
- lib/truffler/clients/cassette.rb
|
|
229
|
+
- lib/truffler/clients/evaluator.rb
|
|
227
230
|
- lib/truffler/clients/fake.rb
|
|
228
231
|
- lib/truffler/clients/ruby_llm_typesafe.rb
|
|
229
232
|
- lib/truffler/configuration.rb
|