whoosh 1.0.2 → 1.2.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: 6d739583e7066d3f04675dbd6eb4e17809226c909f5c7d4d226b7672957e4a88
4
- data.tar.gz: 5f12610ddf11479be668f3b0dc88de6f6c3f5640620299ca3512f48ffaf14922
3
+ metadata.gz: 133af39d011ac77fc6e0773ea19652cb990f5c8d129d40b7f301796062e23e1f
4
+ data.tar.gz: 5145a7c3177f2ddef7667f8faa6a6c2681b38fd279351c95ceda93ce69999c2a
5
5
  SHA512:
6
- metadata.gz: 305d2ea3aac7ba3a22be62c52c996aca50956cce5a0983a8d38cc0f76a55a817c25bdb3fe8877127752a0f3d1e3d81d894c80dc395ae4da39d562d2fad58098f
7
- data.tar.gz: 2cd02f1ab2374bd1334d6dc6bab05fc460e108081eadb8119c9082af41f522c7fb5a8e7f63781b9451aeb319cd001522b7d6998290c662b3c8dbf0706ac03308
6
+ metadata.gz: 4e99f75634ea05e7e2673164f6bdd8b71ea704e6ee02023370b8b77207666bee6291a02d7423d8d036c4014651ba17673c3f23639e64a2a68b5038799b1dca99
7
+ data.tar.gz: 4427cce1a58e9d35841a372ca782f831d6aa3ef513e2309f14e4f6b284a4d12461b102fe5a963768e1a01af2e98db7fc82f7084e970f1a150ef7092f2033c8bf
data/lib/whoosh/app.rb CHANGED
@@ -29,6 +29,7 @@ module Whoosh
29
29
  auto_register_database
30
30
  auto_register_storage
31
31
  auto_register_http
32
+ auto_register_vectors
32
33
  auto_configure_jobs
33
34
  @metrics = Metrics.new
34
35
  auto_register_metrics
@@ -302,8 +303,12 @@ module Whoosh
302
303
  @di.provide(:http) { HTTP }
303
304
  end
304
305
 
306
+ def auto_register_vectors
307
+ @di.provide(:vectors) { VectorStore.build(@config.data) }
308
+ end
309
+
305
310
  def auto_configure_jobs
306
- backend = Jobs::MemoryBackend.new
311
+ backend = Jobs.build_backend(@config.data)
307
312
  Jobs.configure(backend: backend, di: @di)
308
313
  end
309
314
 
@@ -321,7 +326,7 @@ module Whoosh
321
326
  worker = Jobs::Worker.new(
322
327
  backend: Jobs.backend, di: @di,
323
328
  max_retries: max_retries, retry_delay: retry_delay,
324
- instrumentation: @instrumentation
329
+ instrumentation: @instrumentation, logger: @logger
325
330
  )
326
331
  thread = Thread.new { worker.run_loop }
327
332
  thread.abort_on_exception = false
data/lib/whoosh/cache.rb CHANGED
@@ -1,4 +1,3 @@
1
- # lib/whoosh/cache.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Whoosh
@@ -6,20 +5,21 @@ module Whoosh
6
5
  autoload :MemoryStore, "whoosh/cache/memory_store"
7
6
  autoload :RedisStore, "whoosh/cache/redis_store"
8
7
 
8
+ # Auto-detect: REDIS_URL set → Redis, otherwise → Memory
9
9
  def self.build(config_data = {})
10
10
  cache_config = config_data["cache"] || {}
11
- store = cache_config["store"] || "memory"
12
11
  default_ttl = cache_config["default_ttl"] || 300
12
+ redis_url = ENV["REDIS_URL"] || cache_config["url"]
13
13
 
14
- case store
15
- when "memory"
16
- MemoryStore.new(default_ttl: default_ttl)
17
- when "redis"
18
- url = cache_config["url"] || "redis://localhost:6379"
19
- pool_size = cache_config["pool_size"] || 5
20
- RedisStore.new(url: url, default_ttl: default_ttl, pool_size: pool_size)
14
+ if redis_url && cache_config["store"] != "memory"
15
+ begin
16
+ RedisStore.new(url: redis_url, default_ttl: default_ttl)
17
+ rescue Errors::DependencyError
18
+ # Redis gem not installed, fall back to memory
19
+ MemoryStore.new(default_ttl: default_ttl)
20
+ end
21
21
  else
22
- raise ArgumentError, "Unknown cache store: #{store}"
22
+ MemoryStore.new(default_ttl: default_ttl)
23
23
  end
24
24
  end
25
25
  end
@@ -175,8 +175,10 @@ module Whoosh
175
175
  max_connections: 10
176
176
  log_level: debug
177
177
 
178
+ # Cache & Jobs auto-detect:
179
+ # No REDIS_URL → in-memory (just works)
180
+ # Set REDIS_URL → auto-switches to Redis
178
181
  cache:
179
- store: memory
180
182
  default_ttl: 300
181
183
 
182
184
  jobs:
@@ -188,6 +190,12 @@ module Whoosh
188
190
  level: info
189
191
  format: json
190
192
 
193
+ # Vector store auto-detect:
194
+ # zvec gem installed → uses zvec, otherwise → in-memory
195
+ # vector:
196
+ # adapter: auto
197
+ # path: db/vectors
198
+
191
199
  docs:
192
200
  enabled: true
193
201
 
data/lib/whoosh/job.rb CHANGED
@@ -1,4 +1,3 @@
1
- # lib/whoosh/job.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Whoosh
@@ -12,9 +11,41 @@ module Whoosh
12
11
  @dependencies || []
13
12
  end
14
13
 
14
+ def queue(name = nil)
15
+ if name
16
+ @queue_name = name.to_s
17
+ else
18
+ @queue_name || "default"
19
+ end
20
+ end
21
+
22
+ def retry_limit(n = nil)
23
+ if n
24
+ @retry_limit = n
25
+ else
26
+ @retry_limit
27
+ end
28
+ end
29
+
30
+ def retry_backoff(strategy = nil)
31
+ if strategy
32
+ @retry_backoff = strategy
33
+ else
34
+ @retry_backoff || :linear
35
+ end
36
+ end
37
+
15
38
  def perform_async(**args)
16
39
  Jobs.enqueue(self, **args)
17
40
  end
41
+
42
+ def perform_in(delay_seconds, **args)
43
+ Jobs.enqueue(self, run_at: Time.now.to_f + delay_seconds, **args)
44
+ end
45
+
46
+ def perform_at(time, **args)
47
+ Jobs.enqueue(self, run_at: time.to_f, **args)
48
+ end
18
49
  end
19
50
 
20
51
  def perform(**args)
@@ -1,4 +1,3 @@
1
- # lib/whoosh/jobs/memory_backend.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Whoosh
@@ -6,6 +5,7 @@ module Whoosh
6
5
  class MemoryBackend
7
6
  def initialize
8
7
  @queue = []
8
+ @scheduled = []
9
9
  @records = {}
10
10
  @mutex = Mutex.new
11
11
  @cv = ConditionVariable.new
@@ -13,14 +13,25 @@ module Whoosh
13
13
 
14
14
  def push(job_data)
15
15
  @mutex.synchronize do
16
- @queue << job_data
16
+ if job_data[:run_at] && job_data[:run_at] > Time.now.to_f
17
+ @scheduled << job_data
18
+ @scheduled.sort_by! { |j| j[:run_at] }
19
+ else
20
+ @queue << job_data
21
+ end
17
22
  @cv.signal
18
23
  end
19
24
  end
20
25
 
21
26
  def pop(timeout: 5)
22
27
  @mutex.synchronize do
23
- @cv.wait(@mutex, timeout) if @queue.empty?
28
+ # Promote scheduled jobs that are ready
29
+ promote_scheduled
30
+
31
+ if @queue.empty?
32
+ @cv.wait(@mutex, timeout)
33
+ promote_scheduled
34
+ end
24
35
  @queue.shift
25
36
  end
26
37
  end
@@ -34,12 +45,37 @@ module Whoosh
34
45
  end
35
46
 
36
47
  def size
48
+ @mutex.synchronize { @queue.size + @scheduled.size }
49
+ end
50
+
51
+ def pending_count
37
52
  @mutex.synchronize { @queue.size }
38
53
  end
39
54
 
55
+ def scheduled_count
56
+ @mutex.synchronize { @scheduled.size }
57
+ end
58
+
40
59
  def shutdown
41
60
  @mutex.synchronize { @cv.broadcast }
42
61
  end
62
+
63
+ private
64
+
65
+ def promote_scheduled
66
+ now = Time.now.to_f
67
+ ready = []
68
+ remaining = []
69
+ @scheduled.each do |job|
70
+ if job[:run_at] <= now
71
+ ready << job
72
+ else
73
+ remaining << job
74
+ end
75
+ end
76
+ @scheduled = remaining
77
+ @queue.concat(ready)
78
+ end
43
79
  end
44
80
  end
45
81
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whoosh
4
+ module Jobs
5
+ class RedisBackend
6
+ @redis_available = nil
7
+
8
+ def self.available?
9
+ if @redis_available.nil?
10
+ @redis_available = begin
11
+ require "redis"
12
+ true
13
+ rescue LoadError
14
+ false
15
+ end
16
+ end
17
+ @redis_available
18
+ end
19
+
20
+ def initialize(url:, prefix: "whoosh:jobs")
21
+ unless self.class.available?
22
+ raise Errors::DependencyError, "Jobs Redis backend requires the 'redis' gem"
23
+ end
24
+ @redis = Redis.new(url: url)
25
+ @prefix = prefix
26
+ end
27
+
28
+ def push(job_data)
29
+ serialized = Serialization::Json.encode(job_data)
30
+ if job_data[:run_at] && job_data[:run_at] > Time.now.to_f
31
+ # Scheduled: use sorted set with run_at as score
32
+ @redis.zadd("#{@prefix}:scheduled", job_data[:run_at], serialized)
33
+ else
34
+ @redis.lpush("#{@prefix}:queue:#{job_data[:queue] || "default"}", serialized)
35
+ end
36
+ end
37
+
38
+ def pop(timeout: 5, queues: ["default"])
39
+ # First, promote scheduled jobs
40
+ promote_scheduled
41
+
42
+ # Try each queue in priority order
43
+ queues.each do |queue|
44
+ result = @redis.rpop("#{@prefix}:queue:#{queue}")
45
+ if result
46
+ return Serialization::Json.decode(result).transform_keys(&:to_sym)
47
+ end
48
+ end
49
+
50
+ # Block-wait on default queue
51
+ result = @redis.brpop("#{@prefix}:queue:#{queues.first}", timeout: timeout)
52
+ if result
53
+ Serialization::Json.decode(result[1]).transform_keys(&:to_sym)
54
+ end
55
+ rescue => e
56
+ nil
57
+ end
58
+
59
+ def save(record)
60
+ serialized = Serialization::Json.encode(record)
61
+ @redis.set("#{@prefix}:record:#{record[:id]}", serialized, ex: 86400) # 24h TTL
62
+ end
63
+
64
+ def find(id)
65
+ raw = @redis.get("#{@prefix}:record:#{id}")
66
+ return nil unless raw
67
+ data = Serialization::Json.decode(raw)
68
+ data.transform_keys(&:to_sym)
69
+ end
70
+
71
+ def size
72
+ pending_count + scheduled_count
73
+ end
74
+
75
+ def pending_count
76
+ count = 0
77
+ @redis.keys("#{@prefix}:queue:*").each do |key|
78
+ count += @redis.llen(key)
79
+ end
80
+ count
81
+ rescue => e
82
+ 0
83
+ end
84
+
85
+ def scheduled_count
86
+ @redis.zcard("#{@prefix}:scheduled")
87
+ rescue => e
88
+ 0
89
+ end
90
+
91
+ def shutdown
92
+ @redis.close
93
+ rescue => e
94
+ # Already closed
95
+ end
96
+
97
+ private
98
+
99
+ def promote_scheduled
100
+ now = Time.now.to_f
101
+ # Get all jobs ready to run
102
+ ready = @redis.zrangebyscore("#{@prefix}:scheduled", "-inf", now.to_s)
103
+ ready.each do |raw|
104
+ # Remove from scheduled set
105
+ removed = @redis.zrem("#{@prefix}:scheduled", raw)
106
+ next unless removed
107
+
108
+ job_data = Serialization::Json.decode(raw)
109
+ queue = job_data["queue"] || "default"
110
+ @redis.lpush("#{@prefix}:queue:#{queue}", raw)
111
+ end
112
+ rescue => e
113
+ # Don't crash on promote errors
114
+ end
115
+ end
116
+ end
117
+ end
@@ -1,15 +1,15 @@
1
- # lib/whoosh/jobs/worker.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Whoosh
5
4
  module Jobs
6
5
  class Worker
7
- def initialize(backend:, di: nil, max_retries: 3, retry_delay: 5, instrumentation: nil)
6
+ def initialize(backend:, di: nil, max_retries: 3, retry_delay: 5, instrumentation: nil, logger: nil)
8
7
  @backend = backend
9
8
  @di = di
10
9
  @max_retries = max_retries
11
10
  @retry_delay = retry_delay
12
11
  @instrumentation = instrumentation
12
+ @logger = logger
13
13
  @running = true
14
14
  end
15
15
 
@@ -33,13 +33,27 @@ module Whoosh
33
33
 
34
34
  def execute(job_data)
35
35
  id = job_data[:id]
36
+ class_name = job_data[:class_name]
37
+
38
+ # Skip scheduled jobs that aren't ready yet
39
+ if job_data[:run_at] && job_data[:run_at].to_f > Time.now.to_f
40
+ @backend.push(job_data)
41
+ return
42
+ end
43
+
36
44
  record = @backend.find(id) || {}
37
- record = record.merge(status: :running, started_at: Time.now.to_f)
45
+ record = record.merge(id: id, status: :running, started_at: Time.now.to_f)
38
46
  @backend.save(record)
39
47
 
40
- job_class = Object.const_get(job_data[:class_name])
48
+ @logger&.info("job_started", job_id: id, class: class_name)
49
+
50
+ job_class = Object.const_get(class_name)
41
51
  job = job_class.new
42
52
 
53
+ # Determine retry settings from job class or defaults
54
+ max_retries = job_class.respond_to?(:retry_limit) && job_class.retry_limit ? job_class.retry_limit : @max_retries
55
+ backoff_strategy = job_class.respond_to?(:retry_backoff) ? job_class.retry_backoff : :linear
56
+
43
57
  # Inject DI deps
44
58
  if @di && job_class.respond_to?(:dependencies)
45
59
  job_class.dependencies.each do |dep|
@@ -49,23 +63,43 @@ module Whoosh
49
63
  end
50
64
  end
51
65
 
52
- args = job_data[:args].transform_keys(&:to_sym)
66
+ args = job_data[:args]
67
+ args = args.transform_keys(&:to_sym) if args.is_a?(Hash)
53
68
  result = job.perform(**args)
54
69
  serialized = Serialization::Json.decode(Serialization::Json.encode(result))
55
70
 
56
71
  @backend.save(record.merge(status: :completed, result: serialized, completed_at: Time.now.to_f))
72
+ @logger&.info("job_completed", job_id: id, class: class_name)
73
+
57
74
  rescue => e
58
- record = @backend.find(id) || {}
75
+ record = @backend.find(id) || { id: id }
59
76
  retry_count = (record[:retry_count] || 0) + 1
60
77
 
61
- if retry_count <= @max_retries
62
- sleep(@retry_delay) if @retry_delay > 0
63
- @backend.save(record.merge(retry_count: retry_count, status: :pending))
64
- @backend.push(job_data)
78
+ if retry_count <= max_retries
79
+ # Non-blocking retry: re-enqueue with delay timestamp instead of sleeping
80
+ delay = calculate_delay(retry_count, backoff_strategy)
81
+ run_at = Time.now.to_f + delay
82
+ @backend.save(record.merge(retry_count: retry_count, status: :scheduled, run_at: run_at))
83
+ @backend.push(job_data.merge(run_at: run_at))
84
+ @logger&.warn("job_retry", job_id: id, class: class_name, retry_count: retry_count, delay: delay)
65
85
  else
66
86
  error = { message: e.message, backtrace: e.backtrace&.first(10)&.join("\n") }
67
- @backend.save(record.merge(status: :failed, error: error, retry_count: retry_count, completed_at: Time.now.to_f))
87
+ @backend.save(record.merge(
88
+ status: :failed, error: error, retry_count: retry_count, completed_at: Time.now.to_f
89
+ ))
68
90
  @instrumentation&.emit(:job_failed, { job_id: id, error: error })
91
+ @logger&.error("job_failed", job_id: id, class: class_name, error: e.message)
92
+ end
93
+ end
94
+
95
+ def calculate_delay(retry_count, strategy)
96
+ case strategy
97
+ when :exponential
98
+ @retry_delay * (2**(retry_count - 1)) # 5, 10, 20, 40...
99
+ when :linear
100
+ @retry_delay * retry_count # 5, 10, 15, 20...
101
+ else
102
+ @retry_delay
69
103
  end
70
104
  end
71
105
  end
data/lib/whoosh/jobs.rb CHANGED
@@ -1,4 +1,3 @@
1
- # lib/whoosh/jobs.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "securerandom"
@@ -6,6 +5,7 @@ require "securerandom"
6
5
  module Whoosh
7
6
  module Jobs
8
7
  autoload :MemoryBackend, "whoosh/jobs/memory_backend"
8
+ autoload :RedisBackend, "whoosh/jobs/redis_backend"
9
9
  autoload :Worker, "whoosh/jobs/worker"
10
10
 
11
11
  @backend = nil
@@ -23,16 +23,27 @@ module Whoosh
23
23
  !!@backend
24
24
  end
25
25
 
26
- def enqueue(job_class, **args)
26
+ def enqueue(job_class, run_at: nil, **args)
27
27
  raise Errors::DependencyError, "Jobs not configured — boot a Whoosh::App first" unless configured?
28
+
28
29
  id = SecureRandom.uuid
30
+ queue_name = job_class.respond_to?(:queue) ? job_class.queue : "default"
29
31
  record = {
30
- id: id, class_name: job_class.name, args: args, status: :pending,
31
- result: nil, error: nil, retry_count: 0,
32
- created_at: Time.now.to_f, started_at: nil, completed_at: nil
32
+ id: id,
33
+ class_name: job_class.name,
34
+ args: args,
35
+ queue: queue_name,
36
+ status: run_at ? :scheduled : :pending,
37
+ run_at: run_at,
38
+ result: nil,
39
+ error: nil,
40
+ retry_count: 0,
41
+ created_at: Time.now.to_f,
42
+ started_at: nil,
43
+ completed_at: nil
33
44
  }
34
45
  @backend.save(record)
35
- @backend.push({ id: id, class_name: job_class.name, args: args })
46
+ @backend.push({ id: id, class_name: job_class.name, args: args, queue: queue_name, run_at: run_at })
36
47
  id
37
48
  end
38
49
 
@@ -41,6 +52,18 @@ module Whoosh
41
52
  @backend.find(id)
42
53
  end
43
54
 
55
+ # Build the right backend from config (auto-detect pattern)
56
+ def build_backend(config_data = {})
57
+ jobs_config = config_data["jobs"] || {}
58
+ redis_url = ENV["REDIS_URL"] || jobs_config["redis_url"]
59
+
60
+ if redis_url && jobs_config["backend"] != "memory"
61
+ RedisBackend.new(url: redis_url)
62
+ else
63
+ MemoryBackend.new
64
+ end
65
+ end
66
+
44
67
  def reset!
45
68
  @backend = nil
46
69
  @di = nil
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whoosh
4
+ module VectorStore
5
+ class MemoryStore
6
+ def initialize
7
+ @collections = {}
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ # Store a vector with metadata
12
+ def insert(collection, id:, vector:, metadata: {})
13
+ @mutex.synchronize do
14
+ @collections[collection] ||= {}
15
+ @collections[collection][id] = { vector: vector, metadata: metadata }
16
+ end
17
+ end
18
+
19
+ # Search by cosine similarity, return top-k results
20
+ def search(collection, vector:, limit: 10)
21
+ @mutex.synchronize do
22
+ items = @collections[collection]
23
+ return [] unless items && !items.empty?
24
+
25
+ scored = items.map do |id, data|
26
+ score = cosine_similarity(vector, data[:vector])
27
+ { id: id, score: score, metadata: data[:metadata] }
28
+ end
29
+
30
+ scored.sort_by { |r| -r[:score] }.first(limit)
31
+ end
32
+ end
33
+
34
+ # Delete a vector
35
+ def delete(collection, id:)
36
+ @mutex.synchronize do
37
+ @collections[collection]&.delete(id)
38
+ end
39
+ end
40
+
41
+ # Count vectors in a collection
42
+ def count(collection)
43
+ @mutex.synchronize do
44
+ @collections[collection]&.size || 0
45
+ end
46
+ end
47
+
48
+ # Drop a collection
49
+ def drop(collection)
50
+ @mutex.synchronize do
51
+ @collections.delete(collection)
52
+ end
53
+ end
54
+
55
+ def close
56
+ # No-op
57
+ end
58
+
59
+ private
60
+
61
+ def cosine_similarity(a, b)
62
+ return 0.0 if a.empty? || b.empty? || a.length != b.length
63
+
64
+ dot = 0.0
65
+ mag_a = 0.0
66
+ mag_b = 0.0
67
+
68
+ a.length.times do |i|
69
+ dot += a[i] * b[i]
70
+ mag_a += a[i] * a[i]
71
+ mag_b += b[i] * b[i]
72
+ end
73
+
74
+ denom = Math.sqrt(mag_a) * Math.sqrt(mag_b)
75
+ denom.zero? ? 0.0 : dot / denom
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whoosh
4
+ module VectorStore
5
+ autoload :MemoryStore, "whoosh/vector_store/memory_store"
6
+
7
+ # Auto-detect: zvec gem → use it, otherwise → in-memory
8
+ def self.build(config_data = {})
9
+ vector_config = config_data["vector"] || {}
10
+ adapter = vector_config["adapter"] || "auto"
11
+
12
+ case adapter
13
+ when "auto"
14
+ # Try zvec first, fall back to memory
15
+ if zvec_available?
16
+ require "whoosh/vector_store/zvec_store"
17
+ ZvecStore.new(**zvec_options(vector_config))
18
+ else
19
+ MemoryStore.new
20
+ end
21
+ when "memory"
22
+ MemoryStore.new
23
+ when "zvec"
24
+ require "whoosh/vector_store/zvec_store"
25
+ ZvecStore.new(**zvec_options(vector_config))
26
+ else
27
+ MemoryStore.new
28
+ end
29
+ end
30
+
31
+ def self.zvec_available?
32
+ require "zvec"
33
+ true
34
+ rescue LoadError
35
+ false
36
+ end
37
+
38
+ def self.zvec_options(config)
39
+ { path: config["path"] || "db/vectors" }
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whoosh
4
- VERSION = "1.0.2"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/whoosh.rb CHANGED
@@ -28,6 +28,7 @@ module Whoosh
28
28
  autoload :Jobs, "whoosh/jobs"
29
29
  autoload :Metrics, "whoosh/metrics"
30
30
  autoload :Paginate, "whoosh/paginate"
31
+ autoload :VectorStore, "whoosh/vector_store"
31
32
 
32
33
  module Auth
33
34
  autoload :ApiKey, "whoosh/auth/api_key"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whoosh
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johannes Dwi Cahyo
@@ -199,6 +199,7 @@ files:
199
199
  - lib/whoosh/job.rb
200
200
  - lib/whoosh/jobs.rb
201
201
  - lib/whoosh/jobs/memory_backend.rb
202
+ - lib/whoosh/jobs/redis_backend.rb
202
203
  - lib/whoosh/jobs/worker.rb
203
204
  - lib/whoosh/logger.rb
204
205
  - lib/whoosh/mcp/client.rb
@@ -240,6 +241,8 @@ files:
240
241
  - lib/whoosh/test.rb
241
242
  - lib/whoosh/types.rb
242
243
  - lib/whoosh/uploaded_file.rb
244
+ - lib/whoosh/vector_store.rb
245
+ - lib/whoosh/vector_store/memory_store.rb
243
246
  - lib/whoosh/version.rb
244
247
  homepage: https://github.com/johannesdwicahyo/whoosh
245
248
  licenses: