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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd386c4c61e93a3c711116c94d8373b6051d43d46c7387ab86e46e139eaa8c5e
4
- data.tar.gz: d4c490c4faf11f0785172cf8e2d70e53e92652edb59261f0579f6a11c3bf92a2
3
+ metadata.gz: 2dab47f4f45491e8341757cd3a2247c4b75f8384d7b6e4b9dab0ee0624833df6
4
+ data.tar.gz: '045718990a43e85783481fca6e0a3cbcbad7b059f03a03a5f3ab34391a26d952'
5
5
  SHA512:
6
- metadata.gz: 6ccf9b47eeca82500116a7cacb370ba5f2981cc39815b11e7f05c1be826094c0a932ab6226a3436294941af9e63e3be7e1021061f3c4a3a9903422a9b19a9c3d
7
- data.tar.gz: 2bd7415288c8a45291c669d7d7012d676770d967d4954184b309f84350e2b13dd741fea4b79130aa1f857aac288b9d4f083dee00b3011d54a15ee0a53c09672e
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
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  redis:
3
- image: valkey/valkey:9.0.1-alpine
3
+ image: valkey/valkey:9.1.2-alpine
4
4
  restart: unless-stopped
5
5
  ports: [ "127.0.0.1:9736:6379" ]
6
6
  volumes:
data/documentrix.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: documentrix 0.7.0 ruby lib
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.7.0".freeze
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.17".freeze
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).size >= 1 }
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).size >= 1
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).size >= 1
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.
@@ -1,6 +1,6 @@
1
1
  module Documentrix
2
2
  # Documentrix version
3
- VERSION = '0.7.0'
3
+ VERSION = '0.8.0'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -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
@@ -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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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: nil).
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.7.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.17
322
+ rubygems_version: 4.0.20
323
323
  specification_version: 4
324
324
  summary: Ruby library for embedding vector database
325
325
  test_files: