documentrix 0.7.0 → 0.8.0
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/CHANGES.md +20 -0
- data/docker-compose.yml +1 -1
- data/documentrix.gemspec +3 -3
- data/lib/documentrix/documents/cache/common.rb +23 -2
- data/lib/documentrix/documents/cache/sqlite_cache.rb +45 -1
- data/lib/documentrix/documents.rb +11 -0
- data/lib/documentrix/version.rb +1 -1
- data/spec/documentrix/documents/cache/interface_spec.rb +9 -0
- data/spec/documentrix/documents/cache/memory_cache_spec.rb +41 -0
- data/spec/documentrix/documents/cache/redis_cache_spec.rb +91 -0
- data/spec/documentrix/documents/cache/sqlite_cache_spec.rb +58 -0
- data/spec/documents_spec.rb +56 -13
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dab47f4f45491e8341757cd3a2247c4b75f8384d7b6e4b9dab0ee0624833df6
|
|
4
|
+
data.tar.gz: '045718990a43e85783481fca6e0a3cbcbad7b059f03a03a5f3ab34391a26d952'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90096387422aa1fc3678968597bbcefe5cbd20460fe36b0b894c801bff71354ee9c2439d41f8c99e3c8af4c133f07103e2578eed0a2a26dc95954ed8c3eae443
|
|
7
|
+
data.tar.gz: 9a54deddc18ac3ee3b54636f529ee081d4c43f72b1e8f54546be2816fd1fd380439e62d674a5505139266c2ccde73cbeeebe6126ee894e48042e408ce8d71679
|
data/CHANGES.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-10-04 v0.8.0
|
|
4
|
+
|
|
5
|
+
* Added `records(tags:, sources:)` lookup method:
|
|
6
|
+
* New `records(tags:, sources:)` facade in `documents.rb` delegating to
|
|
7
|
+
`@cache`.
|
|
8
|
+
* `Common#records` fallback implementation using `each.select` with
|
|
9
|
+
tag-intersection and source-membership filters, projected via
|
|
10
|
+
`transpose.last.to_a`.
|
|
11
|
+
* `SQLiteCache#records` override using `json_each` + `IN` for precise
|
|
12
|
+
array-element tag matching and `source IN` for multi-source filtering,
|
|
13
|
+
skipping the embedding JOIN.
|
|
14
|
+
* Added comprehensive specs for the new method across `documents_spec.rb`,
|
|
15
|
+
`memory_cache_spec.rb`, `redis_cache_spec.rb`, `sqlite_cache_spec.rb`, and
|
|
16
|
+
`interface_spec.rb`.
|
|
17
|
+
* Upgraded Valkey image from **9.0.1-alpine** to **9.1.2-alpine** in
|
|
18
|
+
`docker-compose.yml`.
|
|
19
|
+
* Refactored tag intersection checks in cache to use `any?`.
|
|
20
|
+
* Updated `ollama` embed call expectations in `spec/documents_spec.rb` to use
|
|
21
|
+
`options: {}` instead of `options: nil`.
|
|
22
|
+
|
|
3
23
|
## 2026-09-12 v0.7.0
|
|
4
24
|
|
|
5
25
|
* `Documents#initialize`: Changed `model_options` default contract from
|
data/docker-compose.yml
CHANGED
data/documentrix.gemspec
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: documentrix 0.
|
|
2
|
+
# stub: documentrix 0.8.0 ruby lib
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = "documentrix".freeze
|
|
6
|
-
s.version = "0.
|
|
6
|
+
s.version = "0.8.0".freeze
|
|
7
7
|
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
9
9
|
s.require_paths = ["lib".freeze]
|
|
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
|
|
|
17
17
|
s.licenses = ["MIT".freeze]
|
|
18
18
|
s.rdoc_options = ["--title".freeze, "Documentrix - Ruby library for embedding vector database".freeze, "--main".freeze, "README.md".freeze]
|
|
19
19
|
s.required_ruby_version = Gem::Requirement.new(">= 3.1".freeze)
|
|
20
|
-
s.rubygems_version = "4.0.
|
|
20
|
+
s.rubygems_version = "4.0.20".freeze
|
|
21
21
|
s.summary = "Ruby library for embedding vector database".freeze
|
|
22
22
|
s.test_files = ["spec/documentrix/documents/cache/interface_spec.rb".freeze, "spec/documentrix/documents/cache/memory_cache_spec.rb".freeze, "spec/documentrix/documents/cache/redis_cache_spec.rb".freeze, "spec/documentrix/documents/cache/sqlite_cache_spec.rb".freeze, "spec/documentrix/documents/splitters/character_spec.rb".freeze, "spec/documentrix/documents/splitters/semantic_spec.rb".freeze, "spec/documents_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/utils/colorize_texts_spec.rb".freeze, "spec/utils/digests_spec.rb".freeze, "spec/utils/tags_spec.rb".freeze]
|
|
23
23
|
|
|
@@ -70,7 +70,7 @@ module Documentrix::Documents::Cache::Common
|
|
|
70
70
|
tags = Documentrix::Utils::Tags.new(Array(tags)).to_a
|
|
71
71
|
records = self
|
|
72
72
|
if tags.present?
|
|
73
|
-
records = records.select { |_key, record| (tags & record.tags).
|
|
73
|
+
records = records.select { |_key, record| (tags & record.tags).any? }
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
needle_norm = norm(needle)
|
|
@@ -88,6 +88,27 @@ module Documentrix::Documents::Cache::Common
|
|
|
88
88
|
max_records ? records.take(max_records) : records
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Returns records filtered by tag and/or source, without vector search.
|
|
92
|
+
#
|
|
93
|
+
# At least one of +tags:+ or +sources:+ should be given to avoid a full
|
|
94
|
+
# collection scan. The +tags:+ parameter accepts a single tag string or an
|
|
95
|
+
# array of tags (OR semantics — a record matching *any* given tag is
|
|
96
|
+
# returned).
|
|
97
|
+
#
|
|
98
|
+
# @param tags [String, Array<String>, nil] tag name(s) to filter by
|
|
99
|
+
# @param sources [String, Array<String>, nil] source(s) to filter by
|
|
100
|
+
# (exact match — a record from *any* given source is returned)
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<Documentrix::Documents::Record>] matching records
|
|
103
|
+
def records(tags: nil, sources: nil)
|
|
104
|
+
tags = Documentrix::Utils::Tags.new(Array(tags)).to_a
|
|
105
|
+
sources = Array(sources)
|
|
106
|
+
each.select do |_key, record|
|
|
107
|
+
(tags.empty? || (tags & record.tags).any?) &&
|
|
108
|
+
(sources.empty? || sources.include?(record.source))
|
|
109
|
+
end.transpose.last.to_a
|
|
110
|
+
end
|
|
111
|
+
|
|
91
112
|
# Returns a set of unique tags found in the cache records.
|
|
92
113
|
#
|
|
93
114
|
# This method iterates through all records in the cache and collects unique
|
|
@@ -112,7 +133,7 @@ module Documentrix::Documents::Cache::Common
|
|
|
112
133
|
# @return [self] self
|
|
113
134
|
def clear_for_tags(tags)
|
|
114
135
|
each do |key, record|
|
|
115
|
-
if (tags & record.tags.to_a).
|
|
136
|
+
if (tags & record.tags.to_a).any?
|
|
116
137
|
delete(unpre(key))
|
|
117
138
|
end
|
|
118
139
|
end
|
|
@@ -341,6 +341,50 @@ class Documentrix::Documents::Cache::SQLiteCache
|
|
|
341
341
|
vector
|
|
342
342
|
end
|
|
343
343
|
|
|
344
|
+
# Returns records filtered by tag and/or source, without vector search.
|
|
345
|
+
#
|
|
346
|
+
# This is a high-performance override for SQLite that uses `json_each`
|
|
347
|
+
# for precise array-element matching, avoiding the embedding JOIN and
|
|
348
|
+
# the Ruby-side post-filter needed by the `LIKE '%tag%'` substring hack.
|
|
349
|
+
#
|
|
350
|
+
# @param tags [String, Array<String>, nil] tag name(s) to filter by
|
|
351
|
+
# (OR semantics — a record matching any given tag is returned)
|
|
352
|
+
# @param sources [String, Array<String>, nil] source(s) to filter by
|
|
353
|
+
# (exact match — a record from *any* given source is returned)
|
|
354
|
+
#
|
|
355
|
+
# @return [Array<Documentrix::Documents::Record>] matching records
|
|
356
|
+
def records(tags: nil, sources: nil)
|
|
357
|
+
tags = Documentrix::Utils::Tags.new(tags).to_a
|
|
358
|
+
sources = Array(sources)
|
|
359
|
+
|
|
360
|
+
where = ['key LIKE ?']
|
|
361
|
+
args = [ start_with_prefix ]
|
|
362
|
+
|
|
363
|
+
unless tags.empty?
|
|
364
|
+
where << "EXISTS (SELECT 1 FROM json_each(tags) " \
|
|
365
|
+
"WHERE json_each.value IN (%s)) " % (%w[ ? ] * tags.size).join(?,)
|
|
366
|
+
args.concat(tags)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
unless sources.empty?
|
|
370
|
+
where << "source IN (%s) " % (%w[ ? ] * sources.size).join(?,)
|
|
371
|
+
args.concat(sources)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
where = where.map { '(%s)' % _1 }
|
|
375
|
+
|
|
376
|
+
execute(
|
|
377
|
+
"SELECT key, text, source, tags, digest " \
|
|
378
|
+
"FROM records WHERE #{where.join(' AND ')}",
|
|
379
|
+
args
|
|
380
|
+
).map do |key, text, source, tags, digest|
|
|
381
|
+
convert_value_to_record(
|
|
382
|
+
key: unpre(key), text:, source:,
|
|
383
|
+
tags: JSON(tags.to_s).to_a, digest:
|
|
384
|
+
)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
344
388
|
# The find_records_for_tags method filters records based on the provided tags.
|
|
345
389
|
#
|
|
346
390
|
# @param tags [ Array ] an array of tag names
|
|
@@ -362,7 +406,7 @@ class Documentrix::Documents::Cache::SQLiteCache
|
|
|
362
406
|
}, [ start_with_prefix ])
|
|
363
407
|
if tags_filter
|
|
364
408
|
records = records.select { |key, tags, embedding_id|
|
|
365
|
-
(tags_filter & JSON(tags.to_s).to_a).
|
|
409
|
+
(tags_filter & JSON(tags.to_s).to_a).any?
|
|
366
410
|
}
|
|
367
411
|
end
|
|
368
412
|
records
|
|
@@ -413,6 +413,17 @@ class Documentrix::Documents
|
|
|
413
413
|
@cache.each_source.to_a
|
|
414
414
|
end
|
|
415
415
|
|
|
416
|
+
# Returns records filtered by tag and/or source, without vector search.
|
|
417
|
+
#
|
|
418
|
+
# @param tags [String, Array<String>, nil] tag name(s) to filter by
|
|
419
|
+
# @param sources [String, Array<String>, nil] source(s) to filter by
|
|
420
|
+
# (exact match — a record from *any* given source is returned)
|
|
421
|
+
#
|
|
422
|
+
# @return [Array<Documentrix::Documents::Record>] matching records
|
|
423
|
+
def records(tags: nil, sources: nil)
|
|
424
|
+
@cache.records(tags:, sources:)
|
|
425
|
+
end
|
|
426
|
+
|
|
416
427
|
# The each_record method iterates over all records stored in the cache.
|
|
417
428
|
#
|
|
418
429
|
# @yield [record] The record being iterated over.
|
data/lib/documentrix/version.rb
CHANGED
|
@@ -58,6 +58,9 @@ describe 'Documentrix::Documents::Cache Interface' do
|
|
|
58
58
|
|
|
59
59
|
expect(cache).to respond_to(:clear)
|
|
60
60
|
expect(cache.method(:clear).owner).to eq Documentrix::Documents::Cache::Common
|
|
61
|
+
|
|
62
|
+
expect(cache).to respond_to(:records)
|
|
63
|
+
expect(cache.method(:records).owner).to eq Documentrix::Documents::Cache::Common
|
|
61
64
|
end
|
|
62
65
|
end
|
|
63
66
|
|
|
@@ -127,6 +130,9 @@ describe 'Documentrix::Documents::Cache Interface' do
|
|
|
127
130
|
expect(cache).to respond_to(:clear)
|
|
128
131
|
expect(cache.method(:clear).owner).to eq Documentrix::Documents::Cache::Common
|
|
129
132
|
|
|
133
|
+
expect(cache).to respond_to(:records)
|
|
134
|
+
expect(cache.method(:records).owner).to eq Documentrix::Documents::Cache::Common
|
|
135
|
+
|
|
130
136
|
# Redis-specific methods
|
|
131
137
|
expect(cache).to respond_to(:redis)
|
|
132
138
|
expect(cache.method(:redis).owner).to eq Documentrix::Documents::RedisCache
|
|
@@ -205,6 +211,9 @@ describe 'Documentrix::Documents::Cache Interface' do
|
|
|
205
211
|
|
|
206
212
|
expect(cache).to respond_to(:find_records_for_tags)
|
|
207
213
|
expect(cache.method(:find_records_for_tags).owner).to eq Documentrix::Documents::Cache::SQLiteCache
|
|
214
|
+
|
|
215
|
+
expect(cache).to respond_to(:records)
|
|
216
|
+
expect(cache.method(:records).owner).to eq Documentrix::Documents::Cache::SQLiteCache
|
|
208
217
|
end
|
|
209
218
|
end
|
|
210
219
|
end
|
|
@@ -197,4 +197,45 @@ describe Documentrix::Documents::MemoryCache do
|
|
|
197
197
|
expect(cache.source_exist?('s1', digest: 'd1', operator: '!=')).to be false
|
|
198
198
|
expect(cache.source_exist?('s1', digest: 'd2', operator: '!=')).to be true
|
|
199
199
|
end
|
|
200
|
+
|
|
201
|
+
describe '#records' do
|
|
202
|
+
before do
|
|
203
|
+
cache['a'] = Documentrix::Documents::Record[text: 'a', source: 's1', tags: ['x', 'y'], embedding: [0.1]]
|
|
204
|
+
cache['b'] = Documentrix::Documents::Record[text: 'b', source: 's1', tags: ['y'], embedding: [0.1]]
|
|
205
|
+
cache['c'] = Documentrix::Documents::Record[text: 'c', source: 's2', tags: ['x'], embedding: [0.1]]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'filters by a single tag' do
|
|
209
|
+
result = cache.records(tags: 'x')
|
|
210
|
+
expect(result.map(&:text)).to match_array(%w[a c])
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'filters by multiple tags with OR semantics' do
|
|
214
|
+
result = cache.records(tags: %w[x y])
|
|
215
|
+
expect(result.map(&:text)).to match_array(%w[a b c])
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'filters by source' do
|
|
219
|
+
result = cache.records(sources: 's2')
|
|
220
|
+
expect(result.map(&:text)).to eq ['c']
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'filters by tag and source' do
|
|
224
|
+
result = cache.records(tags: 'x', sources: 's1')
|
|
225
|
+
expect(result.map(&:text)).to eq ['a']
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it 'filters by multiple sources (OR)' do
|
|
229
|
+
result = cache.records(sources: %w[s1 s2])
|
|
230
|
+
expect(result.map(&:text)).to match_array(%w[a b c])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'returns all records when no filter is given' do
|
|
234
|
+
expect(cache.records.map(&:text)).to match_array(%w[a b c])
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'returns empty array for non-matching tag' do
|
|
238
|
+
expect(cache.records(tags: 'z')).to eq []
|
|
239
|
+
end
|
|
240
|
+
end
|
|
200
241
|
end
|
|
@@ -185,6 +185,97 @@ describe Documentrix::Documents::RedisCache do
|
|
|
185
185
|
expect(cache.source_exist?('s1', digest: 'd2')).to be false
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
+
it 'can filter records by tag' do
|
|
189
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
|
190
|
+
and_yield("#{prefix}foo").and_yield("#{prefix}bar")
|
|
191
|
+
expect(redis).to receive(:get).with("#{prefix}foo").
|
|
192
|
+
and_return(JSON(text: 'a', tags: ['x']))
|
|
193
|
+
expect(redis).to receive(:get).with("#{prefix}bar").
|
|
194
|
+
and_return(JSON(text: 'b', tags: ['y']))
|
|
195
|
+
|
|
196
|
+
results = cache.records(tags: 'x')
|
|
197
|
+
expect(results.map(&:text)).to eq ['a']
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'can filter records by source' do
|
|
201
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
|
202
|
+
and_yield("#{prefix}foo").and_yield("#{prefix}bar")
|
|
203
|
+
expect(redis).to receive(:get).with("#{prefix}foo").
|
|
204
|
+
and_return(JSON(text: 'a', source: 's1'))
|
|
205
|
+
expect(redis).to receive(:get).with("#{prefix}bar").
|
|
206
|
+
and_return(JSON(text: 'b', source: 's2'))
|
|
207
|
+
|
|
208
|
+
results = cache.records(sources: 's1')
|
|
209
|
+
expect(results.map(&:text)).to eq ['a']
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'can filter records by tag and source' do
|
|
213
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
|
214
|
+
and_yield("#{prefix}foo").and_yield("#{prefix}bar")
|
|
215
|
+
.and_yield("#{prefix}baz")
|
|
216
|
+
expect(redis).to receive(:get).with("#{prefix}foo").
|
|
217
|
+
and_return(JSON(text: 'a', source: 's1', tags: ['x']))
|
|
218
|
+
expect(redis).to receive(:get).with("#{prefix}bar").
|
|
219
|
+
and_return(JSON(text: 'b', source: 's1', tags: ['y']))
|
|
220
|
+
expect(redis).to receive(:get).with("#{prefix}baz").
|
|
221
|
+
and_return(JSON(text: 'c', source: 's2', tags: ['x']))
|
|
222
|
+
|
|
223
|
+
results = cache.records(tags: 'x', sources: 's1')
|
|
224
|
+
expect(results.map(&:text)).to eq ['a']
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'can filter records by multiple sources' do
|
|
228
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
|
229
|
+
and_yield("#{prefix}foo").and_yield("#{prefix}bar")
|
|
230
|
+
.and_yield("#{prefix}baz")
|
|
231
|
+
expect(redis).to receive(:get).with("#{prefix}foo").
|
|
232
|
+
and_return(JSON(text: 'a', source: 's1'))
|
|
233
|
+
expect(redis).to receive(:get).with("#{prefix}bar").
|
|
234
|
+
and_return(JSON(text: 'b', source: 's2'))
|
|
235
|
+
expect(redis).to receive(:get).with("#{prefix}baz").
|
|
236
|
+
and_return(JSON(text: 'c', source: 's3'))
|
|
237
|
+
|
|
238
|
+
results = cache.records(sources: %w[ s1 s2 ])
|
|
239
|
+
expect(results.map(&:text)).to match_array %w[ a b ]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it 'can filter records by multiple tags (OR)' do
|
|
243
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
|
244
|
+
and_yield("#{prefix}foo").and_yield("#{prefix}bar")
|
|
245
|
+
.and_yield("#{prefix}baz")
|
|
246
|
+
expect(redis).to receive(:get).with("#{prefix}foo").
|
|
247
|
+
and_return(JSON(text: 'a', tags: ['x']))
|
|
248
|
+
expect(redis).to receive(:get).with("#{prefix}bar").
|
|
249
|
+
and_return(JSON(text: 'b', tags: ['y']))
|
|
250
|
+
expect(redis).to receive(:get).with("#{prefix}baz").
|
|
251
|
+
and_return(JSON(text: 'c', tags: ['z']))
|
|
252
|
+
|
|
253
|
+
results = cache.records(tags: %w[ x z ])
|
|
254
|
+
expect(results.map(&:text)).to match_array %w[ a c ]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'returns all records when no filter given' do
|
|
258
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
|
259
|
+
and_yield("#{prefix}foo").and_yield("#{prefix}bar")
|
|
260
|
+
expect(redis).to receive(:get).with("#{prefix}foo").
|
|
261
|
+
and_return(JSON(text: 'a'))
|
|
262
|
+
expect(redis).to receive(:get).with("#{prefix}bar").
|
|
263
|
+
and_return(JSON(text: 'b'))
|
|
264
|
+
|
|
265
|
+
results = cache.records
|
|
266
|
+
expect(results.map(&:text)).to match_array %w[ a b ]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'only returns records under current prefix' do
|
|
270
|
+
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").
|
|
271
|
+
and_yield("#{prefix}foo")
|
|
272
|
+
expect(redis).to receive(:get).with("#{prefix}foo").
|
|
273
|
+
and_return(JSON(text: 'a'))
|
|
274
|
+
|
|
275
|
+
results = cache.records
|
|
276
|
+
expect(results.map(&:text)).to eq ['a']
|
|
277
|
+
end
|
|
278
|
+
|
|
188
279
|
describe '#collections' do
|
|
189
280
|
it 'extracts unique collection names from keys' do
|
|
190
281
|
expect(redis).to receive(:scan_each).with(match: "#{prefix}*").and_yield(
|
|
@@ -378,4 +378,62 @@ describe Documentrix::Documents::SQLiteCache do
|
|
|
378
378
|
expect(cache.collections('test-')).to eq []
|
|
379
379
|
end
|
|
380
380
|
end
|
|
381
|
+
|
|
382
|
+
describe '#records' do
|
|
383
|
+
before do
|
|
384
|
+
cache['a'] = test_value.merge(text: 'a', source: 's1', tags: %w[ x ])
|
|
385
|
+
cache['b'] = test_value.merge(text: 'b', source: 's1', tags: %w[ y ])
|
|
386
|
+
cache['c'] = test_value.merge(text: 'c', source: 's2', tags: %w[ x z ])
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it 'can filter records by tag' do
|
|
390
|
+
results = cache.records(tags: 'x')
|
|
391
|
+
expect(results.map(&:text)).to match_array %w[ a c ]
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
it 'does not match tags by substring' do
|
|
395
|
+
# 'c' is not a tag, and 'x' should not match 'x'-ish substrings
|
|
396
|
+
expect(cache.records(tags: 'c').map(&:text)).to be_empty
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it 'can filter records by source' do
|
|
400
|
+
results = cache.records(sources: 's1')
|
|
401
|
+
expect(results.map(&:text)).to match_array %w[ a b ]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
it 'can filter records by tag and source' do
|
|
405
|
+
results = cache.records(tags: 'x', sources: 's1')
|
|
406
|
+
expect(results.map(&:text)).to eq ['a']
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
it 'can filter records by multiple sources (OR)' do
|
|
410
|
+
results = cache.records(sources: %w[s1 s2])
|
|
411
|
+
expect(results.map(&:text)).to match_array %w[a b c]
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
it 'can filter records by multiple tags (OR)' do
|
|
415
|
+
results = cache.records(tags: %w[ y z ])
|
|
416
|
+
expect(results.map(&:text)).to match_array %w[ b c ]
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it 'returns all records when no filter given' do
|
|
420
|
+
results = cache.records
|
|
421
|
+
expect(results.map(&:text)).to match_array %w[ a b c ]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it 'returns records without an embedding' do
|
|
425
|
+
record = cache.records(tags: 'x').first
|
|
426
|
+
expect(record).to be_a Documentrix::Documents::Record
|
|
427
|
+
expect(record.embedding).to be_nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it 'is isolated to the current prefix' do
|
|
431
|
+
cache2 = cache.dup
|
|
432
|
+
cache2.prefix = 'other-'
|
|
433
|
+
cache2['d'] = test_value.merge(text: 'd', source: 's9', tags: %w[ x ])
|
|
434
|
+
|
|
435
|
+
expect(cache.records(tags: 'x').map(&:text)).to match_array %w[ a c ]
|
|
436
|
+
expect(cache2.records(tags: 'x').map(&:text)).to eq ['d']
|
|
437
|
+
end
|
|
438
|
+
end
|
|
381
439
|
end
|
data/spec/documents_spec.rb
CHANGED
|
@@ -21,7 +21,7 @@ describe Documentrix::Documents do
|
|
|
21
21
|
|
|
22
22
|
it 'texts can be added to it' do
|
|
23
23
|
expect(ollama).to receive(:embed).
|
|
24
|
-
with(model:, input: %w[ foo bar ], options:
|
|
24
|
+
with(model:, input: %w[ foo bar ], options: {}).
|
|
25
25
|
and_return(double(embeddings: [ [ 0.1 ], [ 0.2 ] ]))
|
|
26
26
|
expect(documents.add(%w[ foo bar ])).to eq documents
|
|
27
27
|
expect(documents.exist?('foo')).to eq true
|
|
@@ -31,7 +31,7 @@ describe Documentrix::Documents do
|
|
|
31
31
|
|
|
32
32
|
it 'a text can be added to it' do
|
|
33
33
|
expect(ollama).to receive(:embed).
|
|
34
|
-
with(model:, input: %w[ foo ], options:
|
|
34
|
+
with(model:, input: %w[ foo ], options: {}).
|
|
35
35
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
36
36
|
expect(documents << 'foo').to eq documents
|
|
37
37
|
expect(documents.exist?('foo')).to eq true
|
|
@@ -41,7 +41,7 @@ describe Documentrix::Documents do
|
|
|
41
41
|
|
|
42
42
|
it 'a text with can be added to it' do
|
|
43
43
|
expect(ollama).to receive(:embed).
|
|
44
|
-
with(model:, input: %w[ foo ], options:
|
|
44
|
+
with(model:, input: %w[ foo ], options: {}).
|
|
45
45
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
46
46
|
expect_any_instance_of(Documentrix::Utils::Tags).to\
|
|
47
47
|
receive(:add).with('bar.rb', source: '/foo/bar.rb')
|
|
@@ -53,11 +53,11 @@ describe Documentrix::Documents do
|
|
|
53
53
|
|
|
54
54
|
it 'can find strings' do
|
|
55
55
|
expect(ollama).to receive(:embed).
|
|
56
|
-
with(model:, input: [ 'foo' ], options:
|
|
56
|
+
with(model:, input: [ 'foo' ], options: {}).
|
|
57
57
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
58
58
|
expect(documents << 'foo').to eq documents
|
|
59
59
|
expect(ollama).to receive(:embed).
|
|
60
|
-
with(model:, input: 'foo', options:
|
|
60
|
+
with(model:, input: 'foo', options: {}).
|
|
61
61
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
62
62
|
records = documents.find('foo')
|
|
63
63
|
expect(records).to eq [
|
|
@@ -68,16 +68,16 @@ describe Documentrix::Documents do
|
|
|
68
68
|
|
|
69
69
|
it 'can find only tagged strings' do
|
|
70
70
|
expect(ollama).to receive(:embed).
|
|
71
|
-
with(model:, input: [ 'foo' ], options:
|
|
71
|
+
with(model:, input: [ 'foo' ], options: {}).
|
|
72
72
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
73
73
|
expect(documents.add('foo', tags: %w[ test ])).to eq documents
|
|
74
74
|
expect(ollama).to receive(:embed).
|
|
75
|
-
with(model:, input: 'foo', options:
|
|
75
|
+
with(model:, input: 'foo', options: {}).
|
|
76
76
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
77
77
|
records = documents.find('foo', tags: %w[ nix ])
|
|
78
78
|
expect(records).to eq []
|
|
79
79
|
expect(ollama).to receive(:embed).
|
|
80
|
-
with(model:, input: 'foo', options:
|
|
80
|
+
with(model:, input: 'foo', options: {}).
|
|
81
81
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
82
82
|
records = documents.find('foo', tags: %w[ test ])
|
|
83
83
|
expect(records).to eq [
|
|
@@ -88,15 +88,15 @@ describe Documentrix::Documents do
|
|
|
88
88
|
|
|
89
89
|
it 'can find strings conditionally' do
|
|
90
90
|
expect(ollama).to receive(:embed).
|
|
91
|
-
with(model:, input: [ 'foobar' ], options:
|
|
91
|
+
with(model:, input: [ 'foobar' ], options: {}).
|
|
92
92
|
and_return(double(embeddings: [ [ 0.01 ] ]))
|
|
93
93
|
expect(ollama).to receive(:embed).
|
|
94
|
-
with(model:, input: [ 'foo' ], options:
|
|
94
|
+
with(model:, input: [ 'foo' ], options: {}).
|
|
95
95
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
96
96
|
expect(documents << 'foobar').to eq documents
|
|
97
97
|
expect(documents << 'foo').to eq documents
|
|
98
98
|
expect(ollama).to receive(:embed).at_least(:once).
|
|
99
|
-
with(model:, input: 'foo', options:
|
|
99
|
+
with(model:, input: 'foo', options: {}).
|
|
100
100
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
101
101
|
records = documents.find_where('foo', text_count: 1)
|
|
102
102
|
expect(records).to eq [
|
|
@@ -117,7 +117,7 @@ describe Documentrix::Documents do
|
|
|
117
117
|
context 'it uses cache' do
|
|
118
118
|
before do
|
|
119
119
|
allow(ollama).to receive(:embed).
|
|
120
|
-
with(model:, input: %w[ foo ], options:
|
|
120
|
+
with(model:, input: %w[ foo ], options: {}).
|
|
121
121
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
122
122
|
end
|
|
123
123
|
|
|
@@ -143,7 +143,7 @@ describe Documentrix::Documents do
|
|
|
143
143
|
|
|
144
144
|
it 'can clear texts with tags' do
|
|
145
145
|
expect(ollama).to receive(:embed).
|
|
146
|
-
with(model:, input: %w[ bar ], options:
|
|
146
|
+
with(model:, input: %w[ bar ], options: {}).
|
|
147
147
|
and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
148
148
|
expect(documents.add('foo', tags: %w[ test ])).to eq documents
|
|
149
149
|
expect(documents.add('bar', tags: %w[ test2 ])).to eq documents
|
|
@@ -234,6 +234,49 @@ describe Documentrix::Documents do
|
|
|
234
234
|
expect(documents.each_record).to be_a Enumerator
|
|
235
235
|
expect(documents.each_record.map(&:text)).to match_array %w[ foo bar ]
|
|
236
236
|
end
|
|
237
|
+
|
|
238
|
+
it 'can retrieve records filtered by tag' do
|
|
239
|
+
allow(ollama).to receive(:embed).and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
240
|
+
documents.add('foo', tags: %w[ test ])
|
|
241
|
+
documents.add('bar', tags: %w[ other ])
|
|
242
|
+
expect(documents.records(tags: 'test').map(&:text)).to eq %w[ foo ]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'can retrieve records matching any of multiple tags' do
|
|
246
|
+
allow(ollama).to receive(:embed).and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
247
|
+
documents.add('foo', tags: %w[ a ])
|
|
248
|
+
documents.add('bar', tags: %w[ b ])
|
|
249
|
+
documents.add('baz', tags: %w[ c ])
|
|
250
|
+
expect(documents.records(tags: %w[ a c ]).map(&:text)).to match_array(%w[ foo baz ])
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'can retrieve records filtered by source' do
|
|
254
|
+
allow(ollama).to receive(:embed).and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
255
|
+
documents.add('foo', source: 's1')
|
|
256
|
+
documents.add('bar', source: 's2')
|
|
257
|
+
expect(documents.records(sources: 's1').map(&:text)).to eq %w[ foo ]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it 'can retrieve records filtered by tag and source' do
|
|
261
|
+
allow(ollama).to receive(:embed).and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
262
|
+
documents.add('foo', tags: %w[ test ], source: 's1')
|
|
263
|
+
documents.add('bar', tags: %w[ test ], source: 's2')
|
|
264
|
+
expect(documents.records(tags: 'test', sources: 's1').map(&:text)).to eq %w[ foo ]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'can retrieve records filtered by multiple sources (OR)' do
|
|
268
|
+
allow(ollama).to receive(:embed).and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
269
|
+
documents.add('foo', source: 's1')
|
|
270
|
+
documents.add('bar', source: 's2')
|
|
271
|
+
documents.add('baz', source: 's3')
|
|
272
|
+
expect(documents.records(sources: %w[ s1 s3 ]).map(&:text)).to match_array(%w[ foo baz ])
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'returns empty records when no records match' do
|
|
276
|
+
allow(ollama).to receive(:embed).and_return(double(embeddings: [ [ 0.1 ] ]))
|
|
277
|
+
documents.add('foo', tags: %w[ test ], source: 's1')
|
|
278
|
+
expect(documents.records(sources: 's2')).to eq []
|
|
279
|
+
end
|
|
237
280
|
end
|
|
238
281
|
|
|
239
282
|
context 'source management' do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: documentrix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Florian Frank
|
|
@@ -319,7 +319,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
319
319
|
- !ruby/object:Gem::Version
|
|
320
320
|
version: '0'
|
|
321
321
|
requirements: []
|
|
322
|
-
rubygems_version: 4.0.
|
|
322
|
+
rubygems_version: 4.0.20
|
|
323
323
|
specification_version: 4
|
|
324
324
|
summary: Ruby library for embedding vector database
|
|
325
325
|
test_files:
|