woods 1.6.1 → 2.0.0.beta2
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 +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Backend Adaptability Matrix
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Decision guidance for picking a vector store, metadata store, graph store, and embedding provider. Covers what's implemented today, what's still a design target, and how the four shipped presets map to `lib/woods/builder.rb`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Persistence story
|
|
10
|
+
|
|
11
|
+
Every backend combination falls into one of three shapes based on how data survives process boundaries. The right shape depends on whether the embed process and the query process share a Ruby VM, a filesystem, or neither.
|
|
12
|
+
|
|
13
|
+
| Shape | Vector store | Metadata store | Durability | Right preset |
|
|
14
|
+
|---|---|---|---|---|
|
|
15
|
+
| **Local artifact** | `:in_memory` + dump to `output_dir` | `:sqlite` under `output_dir` | Reopens from the published output artifact | `:local` |
|
|
16
|
+
| **Shared filesystem** | `:in_memory` + `Snapshotter` dump to `output_dir` | `:in_memory` + `Snapshotter` dump to `output_dir` | Process-local, hydrated from disk on MCP boot; dumps retained per `dump_retention_count` (default 3) | `:shared_filesystem` |
|
|
17
|
+
| **Durable vector backend** | `:pgvector` or `:qdrant` | `:sqlite` under `output_dir` | Vectors are external; metadata/config remain a deployed output artifact | `:postgresql`, `:production` |
|
|
18
|
+
|
|
19
|
+
The shape determines the capability matrix:
|
|
20
|
+
|
|
21
|
+
| Capability | Local artifact | Shared filesystem | Durable vector backend |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| Survives process restart | Yes (via output artifact) | Yes (via dump) | Yes (backend + output artifact) |
|
|
24
|
+
| Multi-writer embedding | No | No (single writer assumed) | Do not assume it for the complete index; coordinate one publisher even if the vector backend supports concurrent writes |
|
|
25
|
+
| Requires sqlite3 gem in host | Yes | No | With `:postgresql`/`:production` |
|
|
26
|
+
| Requires embedding/vector service | Ollama | Ollama | OpenAI plus pgvector or Qdrant |
|
|
27
|
+
| Cross-machine query | After deploying/copying `output_dir` | Yes, when the filesystem is shared | External vectors are shared; metadata/config still require a shared or deployed `output_dir` |
|
|
28
|
+
| `woods.json` schema-versioned config snapshot | Yes | Yes | Yes |
|
|
29
|
+
|
|
30
|
+
`Builder#build_vector_store` accepts exactly `:in_memory`, `:pgvector`, `:qdrant`, anything else raises `ArgumentError: Unknown vector_store`. `build_metadata_store` accepts `:in_memory`, `:sqlite`. `build_graph_store` accepts `:in_memory` only. Presets are `:local`, `:shared_filesystem`, `:postgresql`, and `:production`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Vector stores
|
|
35
|
+
|
|
36
|
+
### Database compatibility
|
|
37
|
+
|
|
38
|
+
The vector store you can use depends on the primary database your Rails app uses. MySQL stacks **must** pair with an external vector backend; PostgreSQL stacks have the option of running pgvector inside the same database.
|
|
39
|
+
|
|
40
|
+
| Primary database | Supported vector stores | Required? |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **MySQL / Percona / MariaDB / Aurora MySQL** | `:qdrant` (external); `:in_memory` (local dev only) | Yes. MySQL has no native vector extension |
|
|
43
|
+
| **PostgreSQL / Aurora PostgreSQL** | `:pgvector` (in-database), `:qdrant`; `:in_memory` (local dev only) | No, `:pgvector` runs inside the same database |
|
|
44
|
+
|
|
45
|
+
**Why MySQL needs an external backend.** MySQL ships no equivalent of the `pgvector` extension. Approximate-nearest-neighbour search over arbitrary float vectors is not part of InnoDB / MyISAM and cannot be added via plugin. Woods does not emulate vector search in MySQL, the gem only ships adapters that delegate to a real vector engine. The shipped pairing for MySQL apps is `:qdrant` for vectors with Woods' own `:sqlite` metadata store; Woods never stores metadata in your application database.
|
|
46
|
+
|
|
47
|
+
### pgvector (PostgreSQL extension)
|
|
48
|
+
|
|
49
|
+
**What it is:** PostgreSQL extension that adds vector similarity search directly to Postgres.
|
|
50
|
+
|
|
51
|
+
**Best for:** Teams already on PostgreSQL who want to minimize infrastructure. Rails 8 apps with the Solid suite. Codebases under ~5000 units.
|
|
52
|
+
|
|
53
|
+
**Strengths:**
|
|
54
|
+
- Zero additional infrastructure if you're on PostgreSQL
|
|
55
|
+
- Transactional consistency with metadata (same database)
|
|
56
|
+
- Familiar SQL interface, works with ActiveRecord
|
|
57
|
+
- Supports HNSW indexing
|
|
58
|
+
- Backed by strong open-source community
|
|
59
|
+
|
|
60
|
+
**Weaknesses:**
|
|
61
|
+
- Search performance degrades at high scale (>100K vectors) without careful tuning
|
|
62
|
+
- HNSW index builds are memory-intensive
|
|
63
|
+
- Competes for resources with your application database
|
|
64
|
+
- No built-in sharding for vectors
|
|
65
|
+
|
|
66
|
+
**Configuration:**
|
|
67
|
+
```ruby
|
|
68
|
+
config.vector_store = :pgvector
|
|
69
|
+
# pgvector needs a live PostgreSQL connection object (not a URL string).
|
|
70
|
+
# When your app runs on PostgreSQL, reuse its connection:
|
|
71
|
+
config.vector_store_options = { connection: ActiveRecord::Base.connection }
|
|
72
|
+
|
|
73
|
+
# Dedicated vector database: e.g. a MySQL app pointing at a separate
|
|
74
|
+
# PostgreSQL store: via an abstract class that owns its own connection:
|
|
75
|
+
# class VectorDatabase < ActiveRecord::Base
|
|
76
|
+
# self.abstract_class = true
|
|
77
|
+
# establish_connection(ENV.fetch("VECTOR_DATABASE_URL"))
|
|
78
|
+
# end
|
|
79
|
+
# config.vector_store_options = { connection: VectorDatabase.connection }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`vector_store_options` also accepts `:table` (default `woods_vectors`) and `:schema` (both optional); `:dimensions` is inferred from the embedding provider. `Builder#build_pgvector_store` requires `vector_store_options[:connection]` and raises if it is missing.
|
|
83
|
+
|
|
84
|
+
**Schema** (what `Woods::Storage::VectorStore::Pgvector#ensure_schema!` actually creates, safe to call repeatedly, uses `IF NOT EXISTS`):
|
|
85
|
+
```sql
|
|
86
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
87
|
+
|
|
88
|
+
CREATE TABLE IF NOT EXISTS woods_vectors (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
embedding vector(1536),
|
|
91
|
+
metadata JSONB DEFAULT '{}',
|
|
92
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_woods_vectors_embedding_hnsw
|
|
96
|
+
ON woods_vectors USING hnsw (embedding vector_cosine_ops);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Performance notes:**
|
|
100
|
+
- HNSW: ~5ms search at 10K vectors, ~20ms at 100K. Memory: ~1.5x vector size.
|
|
101
|
+
- For codebase indexing (~1000-5000 units, potentially 5000-20000 chunks), HNSW is appropriate.
|
|
102
|
+
- Recommend a separate database from your application if running in production.
|
|
103
|
+
|
|
104
|
+
**When to use:** PostgreSQL is your primary database, you value simplicity, and scale is under ~50K vectors.
|
|
105
|
+
|
|
106
|
+
**When to avoid:** MySQL is your primary database (can't use pgvector), you need sub-millisecond search, or you're indexing multiple large codebases.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### Qdrant
|
|
111
|
+
|
|
112
|
+
**What it is:** Purpose-built vector database with native filtering, written in Rust.
|
|
113
|
+
|
|
114
|
+
**Best for:** Teams with Docker-based infrastructure who want dedicated vector search. Self-hosted environments. Multi-codebase indexing.
|
|
115
|
+
|
|
116
|
+
**Strengths:**
|
|
117
|
+
- Purpose-built for vector search (consistently fast)
|
|
118
|
+
- Native payload filtering (no joins needed)
|
|
119
|
+
- Built-in quantization for memory efficiency
|
|
120
|
+
- Excellent Docker support, trivial to add to docker-compose
|
|
121
|
+
- gRPC and REST APIs
|
|
122
|
+
|
|
123
|
+
**Weaknesses:**
|
|
124
|
+
- Additional infrastructure to manage
|
|
125
|
+
- Separate from your application database (no transactional consistency)
|
|
126
|
+
- Overkill for small codebases
|
|
127
|
+
|
|
128
|
+
**Configuration:**
|
|
129
|
+
```ruby
|
|
130
|
+
config.vector_store = :qdrant
|
|
131
|
+
config.vector_store_options = {
|
|
132
|
+
url: ENV.fetch("QDRANT_URL", "http://localhost:6333"),
|
|
133
|
+
collection: "woods",
|
|
134
|
+
api_key: ENV["QDRANT_API_KEY"], # optional; omit for unauthenticated local instances
|
|
135
|
+
dimensions: 1_536, # optional; pre-validates vector length client-side
|
|
136
|
+
distance: "Cosine", # Cosine, Dot, Euclid, or Manhattan; verified on reopen
|
|
137
|
+
allow_private_hosts: true # required for localhost/RFC1918 URLs, the SSRF guard blocks them by default
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The adapter constructor takes these as keyword arguments (`Woods::Storage::VectorStore::Qdrant`); `Builder#build_vector_store` splats `vector_store_options` straight into it. Works identically whether your application database is MySQL or PostgreSQL. Qdrant is a separate service either way.
|
|
142
|
+
|
|
143
|
+
When an installed `woods-mcp` process reopens `woods.json` without the host initializer, non-secret options such as collection, distance, table, schema, and dimensions come from the snapshot. Credentials and process-specific connections remain serve-time settings:
|
|
144
|
+
|
|
145
|
+
- `OPENAI_API_KEY` supplies the embedding credential for OpenAI snapshots.
|
|
146
|
+
- Qdrant endpoint URLs and API keys are never stored in `woods.json`. `WOODS_QDRANT_URL` is required when serving a Qdrant index; `WOODS_QDRANT_API_KEY` is optional, and `WOODS_QDRANT_COLLECTION` supplies a collection only when the snapshot does not record one.
|
|
147
|
+
- `WOODS_PG_URL` is required to construct the Active Record connection for a pgvector snapshot outside its host application.
|
|
148
|
+
|
|
149
|
+
SQLite metadata is always reopened as `metadata.sqlite3` beneath the supplied index directory, never relative to the MCP process working directory.
|
|
150
|
+
|
|
151
|
+
**Point IDs.** Qdrant accepts only an unsigned integer or a UUID as a point id, so the adapter cannot store a Woods identifier directly. It derives a deterministic UUIDv5 from the identifier over a pinned namespace (`Qdrant::POINT_ID_NAMESPACE`) and carries the identifier in the payload under `woods_identifier`; `#search` reverse-maps hits back to identifiers and `#delete` translates through the same function. The namespace must never change: a v5 id is what makes re-embedding an unchanged unit *replace* its point instead of adding a second one. See #147.
|
|
152
|
+
|
|
153
|
+
**Sharing a collection.** `#each_id` enumerates only points carrying a `woods_identifier` payload — points another writer put in the same collection are skipped, not yielded. It backs the embed pipeline's staleness sweep, which deletes anything it enumerates that extraction no longer holds, so yielding a foreign point would destroy another system's vectors on every run. The Indexer applies the same rule a second time, ignoring vanished ids shaped like a canonical UUID or an integer — shapes Woods never mints as an identifier.
|
|
154
|
+
|
|
155
|
+
**Docker Compose:**
|
|
156
|
+
```yaml
|
|
157
|
+
services:
|
|
158
|
+
qdrant:
|
|
159
|
+
image: qdrant/qdrant:v1.12.1
|
|
160
|
+
ports:
|
|
161
|
+
- "6333:6333"
|
|
162
|
+
- "6334:6334"
|
|
163
|
+
volumes:
|
|
164
|
+
- qdrant-data:/qdrant/storage
|
|
165
|
+
environment:
|
|
166
|
+
QDRANT__SERVICE__GRPC_PORT: 6334
|
|
167
|
+
deploy:
|
|
168
|
+
resources:
|
|
169
|
+
limits:
|
|
170
|
+
memory: 512M # Sufficient for ~50K vectors
|
|
171
|
+
|
|
172
|
+
volumes:
|
|
173
|
+
qdrant-data:
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Performance notes:**
|
|
177
|
+
- ~2ms search at 10K vectors, ~5ms at 100K
|
|
178
|
+
- Memory: ~100MB for 10K 1536-dim vectors with HNSW
|
|
179
|
+
- Quantization can reduce memory by 4x with minimal quality loss
|
|
180
|
+
|
|
181
|
+
**When to use:** Docker infrastructure, self-hosted, need for filtered search, multi-codebase, or want separation of concerns between app DB and vector search.
|
|
182
|
+
|
|
183
|
+
**When to avoid:** Minimal infrastructure footprint is a priority, or team doesn't want another service to manage.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### Not implemented
|
|
188
|
+
|
|
189
|
+
These are aspirational; setting `config.vector_store` to any of them raises `ArgumentError: Unknown vector_store`, and there is no `vector_store_api_key` / `vector_store_environment` / `vector_store_index` accessor on `Configuration`, code written against the examples below will not run against the shipped gem. The interface a real adapter must implement is `Woods::Storage::VectorStore::Interface` (`store`, `search`, `delete`, `each_id`); see `Woods::Storage::VectorStore::Pgvector` or `Qdrant` for a working example to model a new adapter on.
|
|
190
|
+
|
|
191
|
+
| Backend | Status | Notes |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| Pinecone | Planned (#83) | Managed cloud vector DB. Would suit teams that want zero ops and accept vendor lock-in and data leaving the infrastructure. |
|
|
194
|
+
| SQLite-vss / FAISS | Planned | File-based local vector search. `:in_memory` (the `:local` preset) already covers the zero-dependency local case today. |
|
|
195
|
+
| Chroma | Not planned | Ruby client is third-party and less mature than the Qdrant/pgvector tooling already shipped. |
|
|
196
|
+
| Milvus | Not planned | Massive-scale, multi-tenant vector DB. Only worth building if a host needs billions of vectors or GPU-accelerated search across many codebases, well beyond single-codebase indexing. |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Embedding providers
|
|
201
|
+
|
|
202
|
+
### OpenAI text-embedding-3-small
|
|
203
|
+
|
|
204
|
+
**Dimensions:** 1536
|
|
205
|
+
**Max tokens:** 8191
|
|
206
|
+
**Cost:** ~$0.02 per 1M tokens
|
|
207
|
+
**Latency:** ~100ms single, ~500ms batch of 100
|
|
208
|
+
|
|
209
|
+
**Strengths:** Good quality/cost ratio, fast, well-documented, reliable API.
|
|
210
|
+
**Weaknesses:** Data sent to OpenAI, API dependency, not code-optimized.
|
|
211
|
+
|
|
212
|
+
**Best for:** General use, getting started, teams already using OpenAI.
|
|
213
|
+
|
|
214
|
+
### OpenAI text-embedding-3-large
|
|
215
|
+
|
|
216
|
+
**Dimensions:** 3072
|
|
217
|
+
**Max tokens:** 8191
|
|
218
|
+
**Cost:** ~$0.13 per 1M tokens
|
|
219
|
+
**Latency:** ~150ms single, ~800ms batch of 100
|
|
220
|
+
|
|
221
|
+
**Strengths:** Higher quality than small, supports dimension reduction (can use 1536 dims for compatibility).
|
|
222
|
+
**Weaknesses:** 6.5x cost of small, marginal quality improvement for code.
|
|
223
|
+
|
|
224
|
+
**Best for:** When retrieval quality is paramount and cost is not a concern.
|
|
225
|
+
|
|
226
|
+
### Ollama (self-hosted)
|
|
227
|
+
|
|
228
|
+
| Model | Native context | Dimensions | Weights | Notes |
|
|
229
|
+
|---|---|---|---|---|
|
|
230
|
+
| `nomic-embed-text` (default) | 2048 | 768 | 274 MB | General-purpose; pull from Ollama before first use |
|
|
231
|
+
| `bge-m3` | **8192** | 1024 | 1.2 GB | Fewer chunks per unit, stronger code-search benchmarks |
|
|
232
|
+
| `snowflake-arctic-embed2` | 8192 | 1024 | 1.2 GB | Multilingual variant of bge-m3 |
|
|
233
|
+
| `mxbai-embed-large` | 512 | 1024 | 670 MB | Best for short text |
|
|
234
|
+
| `all-minilm` | 512 | 384 | 46 MB | Tight-memory environments |
|
|
235
|
+
|
|
236
|
+
**Cost:** Hardware only
|
|
237
|
+
**Latency:** ~200ms single (GPU), ~2s single (CPU)
|
|
238
|
+
|
|
239
|
+
**Strengths:** Fully self-hosted, no data leaves infrastructure, no API costs, works offline.
|
|
240
|
+
**Weaknesses:** Requires GPU for reasonable performance (CPU is 10x slower). `nomic-embed-text`'s 2048-token ceiling requires chunking most real-world Rails units, switch to `bge-m3` for fewer chunks if disk space allows.
|
|
241
|
+
|
|
242
|
+
**Best for:** Security-sensitive environments, air-gapped networks, cost-sensitive at scale.
|
|
243
|
+
|
|
244
|
+
> Ollama's `/api/embed` enforces the model's native context length regardless of the `options.num_ctx` override ([ollama/ollama#14186](https://github.com/ollama/ollama/issues/14186)). Woods advertises the native ceiling per model so the chunker sizes inputs correctly, see [EMBEDDING_MODELS.md](EMBEDDING_MODELS.md).
|
|
245
|
+
|
|
246
|
+
### Implemented provider boundary
|
|
247
|
+
|
|
248
|
+
`Builder#build_embedding_provider` accepts `:openai`, `:ollama`, and `:fake`.
|
|
249
|
+
The fake provider is deterministic and offline for specs, CI, and sandbox
|
|
250
|
+
contract tests; it does not represent semantic quality. Other values raise
|
|
251
|
+
`ArgumentError`. Voyage Code 3/2 and Anthropic embeddings are not wired up:
|
|
252
|
+
|
|
253
|
+
- **Voyage Code 3 / Code 2**: code-specialized embeddings (1024/1536 dims, up to 32K token context). Would be the best-quality option for code retrieval if implemented; there is no `Woods::Embedding::Provider::Voyage` today.
|
|
254
|
+
- **Anthropic**: Anthropic does not currently offer a standalone embedding API. Monitor for availability.
|
|
255
|
+
|
|
256
|
+
### Embedding selection guidance (implemented providers only)
|
|
257
|
+
|
|
258
|
+
| Priority | Recommendation |
|
|
259
|
+
|----------|---------------|
|
|
260
|
+
| **Best general-purpose** | OpenAI text-embedding-3-small |
|
|
261
|
+
| **Lowest cost / no external dependencies** | Ollama + `nomic-embed-text` |
|
|
262
|
+
| **Self-hosted + large units** | Ollama + `bge-m3` (8192-token context vs. 2048) |
|
|
263
|
+
| **Maximum quality** | OpenAI text-embedding-3-large |
|
|
264
|
+
| **Offline deterministic tests** | `:fake` (contract testing only, not semantic ranking) |
|
|
265
|
+
|
|
266
|
+
**Critical consideration:** Embedding dimensions must match across your entire index. Changing embedding providers or models requires a full re-index, `rake woods:embed` raises `Woods::MCP::DimensionMismatch` before embedding anything when the configured provider's dimension disagrees with the store's.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Metadata stores
|
|
271
|
+
|
|
272
|
+
`build_metadata_store` accepts `:in_memory` and `:sqlite`. Nothing else is implemented.
|
|
273
|
+
|
|
274
|
+
### SQLite
|
|
275
|
+
|
|
276
|
+
**Best for:** Local development, zero-dependency setups, testing, and every shipped preset except pure in-memory.
|
|
277
|
+
|
|
278
|
+
**Key features:**
|
|
279
|
+
- JSON1 extension for metadata queries
|
|
280
|
+
- FTS5 for full-text search
|
|
281
|
+
- Zero setup, single-file database (`metadata.sqlite3` under `output_dir`)
|
|
282
|
+
|
|
283
|
+
**Limitations:**
|
|
284
|
+
- Single writer at a time
|
|
285
|
+
- No network access
|
|
286
|
+
|
|
287
|
+
### In-memory
|
|
288
|
+
|
|
289
|
+
**Best for:** Testing, evaluation, small codebases.
|
|
290
|
+
|
|
291
|
+
Loads from extracted JSON files on startup. All queries run against in-memory hash maps. Fast but ephemeral, nothing survives a process restart without the `Snapshotter` dump (see the Persistence Story table above).
|
|
292
|
+
|
|
293
|
+
### Not implemented
|
|
294
|
+
|
|
295
|
+
A PostgreSQL or MySQL metadata store (JSONB/JSON columns, generated columns, full-text search, recursive-CTE graph dual-use) is a plausible future adapter, `config.metadata_store = :postgresql` or `:mysql` today raises `ArgumentError: Unknown metadata_store`, and `metadata_store_connection` is not a `Configuration` accessor. For MySQL- or PostgreSQL-backed deployments today, pair `:sqlite` metadata with your vector store of choice (`:pgvector` or `:qdrant`).
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Graph stores
|
|
300
|
+
|
|
301
|
+
`build_graph_store` accepts `:in_memory` only.
|
|
302
|
+
|
|
303
|
+
### In-memory (the only shipped graph store)
|
|
304
|
+
|
|
305
|
+
Loads `dependency_graph.json` into a Ruby hash structure. Supports BFS traversal with visited set, PageRank scoring, and structural analysis via `GraphAnalyzer` (orphan detection, dead-end detection, hub identification, cycle detection, bridge detection). Suitable for up to ~5000 nodes.
|
|
306
|
+
|
|
307
|
+
**Memory:** ~10MB for 2000 nodes with average 5 edges each.
|
|
308
|
+
**Traversal:** < 1ms for depth-2 BFS.
|
|
309
|
+
**Analysis:** `GraphAnalyzer` provides `orphans`, `dead_ends`, `hubs(limit:)`, `cycles`, `bridges(limit:, sample_size:)`, `domain_clusters`, `cross_database_edges`, `volatile_dependencies`, `undeclared_package_edges`, and a combined `analyze` method.
|
|
310
|
+
|
|
311
|
+
### Not implemented
|
|
312
|
+
|
|
313
|
+
A recursive-CTE graph store (MySQL 8.0+ or PostgreSQL, storing edges in a table and traversing with `WITH RECURSIVE`) or Neo4j would only matter past ~50,000 nodes, or for cross-repository tracing and algorithms beyond PageRank/hub/bridge/cycle detection (weighted shortest path, community detection). Neither exists in the shipped gem; `config.graph_store` set to anything but `:in_memory` raises.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Background job integration
|
|
318
|
+
|
|
319
|
+
Indexing can be triggered synchronously (rake task, inline) or from a background job. The pipeline itself is job-system-agnostic, it's synchronous Ruby, and the wrapper below is just scheduling and concurrency control. Use `Woods.extract!` for a full run; incremental runs need a changed-file list, so a job usually just shells out to `rake woods:incremental` (which computes that list from git) rather than calling `Woods.extract_changed!` directly.
|
|
320
|
+
|
|
321
|
+
### Sidekiq
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
class WoodsJob
|
|
325
|
+
include Sidekiq::Job
|
|
326
|
+
sidekiq_options queue: :low, retry: 2
|
|
327
|
+
|
|
328
|
+
def perform(mode = "full")
|
|
329
|
+
case mode
|
|
330
|
+
when "full" then Woods.extract!
|
|
331
|
+
when "incremental" then Rake::Task["woods:incremental"].invoke
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Solid queue (Rails 8)
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
class WoodsJob < ApplicationJob
|
|
341
|
+
queue_as :low_priority
|
|
342
|
+
limits_concurrency to: 1, key: "woods"
|
|
343
|
+
|
|
344
|
+
def perform(mode = "full")
|
|
345
|
+
case mode
|
|
346
|
+
when "full" then Woods.extract!
|
|
347
|
+
when "incremental" then Rake::Task["woods:incremental"].invoke
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### GoodJob
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
class WoodsJob < ApplicationJob
|
|
357
|
+
queue_as :utility
|
|
358
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 3
|
|
359
|
+
|
|
360
|
+
def perform(mode = "full")
|
|
361
|
+
# Same interface as above
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Inline (Development/CI)
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
# No job system needed
|
|
370
|
+
Woods.extract!
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Recommended stack combinations
|
|
376
|
+
|
|
377
|
+
### Starter (local dependencies)
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
Woods.configure_with_preset(:local)
|
|
381
|
+
# Vector: InMemory VectorStore
|
|
382
|
+
# Metadata: SQLite
|
|
383
|
+
# Graph: In-memory
|
|
384
|
+
# Embedding: Ollama (nomic-embed-text)
|
|
385
|
+
# Jobs: Inline
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Setup:** add the `sqlite3` gem, then install/start Ollama and run
|
|
389
|
+
`ollama pull nomic-embed-text`.
|
|
390
|
+
**Tradeoff:** Lower retrieval quality, CPU-bound embedding, single-user.
|
|
391
|
+
|
|
392
|
+
### Rails 8 standard
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
Woods.configure_with_preset(:postgresql) do |config|
|
|
396
|
+
config.embedding_options = { api_key: ENV.fetch('OPENAI_API_KEY') }
|
|
397
|
+
config.vector_store_options = { connection: ActiveRecord::Base.connection }
|
|
398
|
+
end
|
|
399
|
+
# Vector: pgvector
|
|
400
|
+
# Metadata: SQLite
|
|
401
|
+
# Graph: In-memory
|
|
402
|
+
# Embedding: OpenAI
|
|
403
|
+
# Jobs: Solid Queue
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Setup:** `bundle add pgvector` + enable extension
|
|
407
|
+
**Tradeoff:** All-in-one database, good quality, API dependency for embeddings.
|
|
408
|
+
|
|
409
|
+
### MySQL + Qdrant (classic Rails)
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
# No dedicated :mysql preset exists. Use :production and reuse your MySQL
|
|
413
|
+
# connection for the app itself: Woods' own metadata store stays SQLite.
|
|
414
|
+
Woods.configure_with_preset(:production) do |config|
|
|
415
|
+
config.embedding_options = { api_key: ENV.fetch('OPENAI_API_KEY') }
|
|
416
|
+
config.vector_store_options = {
|
|
417
|
+
url: ENV.fetch('QDRANT_URL'),
|
|
418
|
+
collection: ENV.fetch('WOODS_QDRANT_COLLECTION', 'woods'),
|
|
419
|
+
allow_private_hosts: true # only for a deliberately trusted private endpoint
|
|
420
|
+
}
|
|
421
|
+
end
|
|
422
|
+
# Vector: Qdrant
|
|
423
|
+
# Metadata: SQLite
|
|
424
|
+
# Graph: In-memory
|
|
425
|
+
# Embedding: OpenAI
|
|
426
|
+
# Jobs: Sidekiq
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Setup:** Add Qdrant to docker-compose.
|
|
430
|
+
**Tradeoff:** Leverages existing MySQL infrastructure for the app; Qdrant handles vector search, which MySQL can't do natively. Most natural fit for established Rails apps on MySQL/Percona with Docker and Sidekiq.
|
|
431
|
+
|
|
432
|
+
### Fully self-hosted
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
# No dedicated :self_hosted preset exists. Use :production, then override
|
|
436
|
+
# embedding_provider to :ollama.
|
|
437
|
+
Woods.configure_with_preset(:production) do |config|
|
|
438
|
+
config.embedding_provider = :ollama
|
|
439
|
+
config.embedding_options = {
|
|
440
|
+
model: 'nomic-embed-text',
|
|
441
|
+
host: ENV.fetch('OLLAMA_URL', 'http://localhost:11434')
|
|
442
|
+
}
|
|
443
|
+
config.vector_store_options = {
|
|
444
|
+
url: ENV.fetch('QDRANT_URL'),
|
|
445
|
+
collection: ENV.fetch('WOODS_QDRANT_COLLECTION', 'woods'),
|
|
446
|
+
allow_private_hosts: true # Qdrant is deliberately self-hosted/private here
|
|
447
|
+
}
|
|
448
|
+
end
|
|
449
|
+
# Vector: Qdrant
|
|
450
|
+
# Metadata: SQLite
|
|
451
|
+
# Graph: In-memory
|
|
452
|
+
# Embedding: Ollama (nomic-embed-text or bge-m3)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Setup:** Qdrant + Ollama in docker-compose.
|
|
456
|
+
**Tradeoff:** No external API calls, all data stays on-premise. Works with either database. Embedding quality depends on model choice.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Cost and scale guidance
|
|
461
|
+
|
|
462
|
+
Embedding and vector-storage cost are not the bottleneck for a single-codebase index, the numbers below are the reasoning, not a budgeting exercise.
|
|
463
|
+
|
|
464
|
+
- **Embedding cost is driven by token count, not unit count.** Each unit produces roughly 1–4 chunks (hierarchical chunking: one summary chunk plus semantic sub-chunks), each with a small context prefix. At OpenAI's `text-embedding-3-small` price (~$0.02/1M tokens), a full re-index of a codebase in the thousands-of-units range costs cents, not dollars. Ollama costs nothing per token but requires GPU time.
|
|
465
|
+
- **Incremental re-embedding is cheaper still.** Chunk checksumming means only chunks whose source actually changed get re-embedded, a typical merge touches a handful of units.
|
|
466
|
+
- **Query-time embedding cost scales with query volume, not index size.** One embedding call per `codebase_retrieve` query; even four-digit daily query volumes stay cheap with the small model.
|
|
467
|
+
- **Vector storage is `dimensions × 4 bytes` per vector**, plus adapter overhead. At 1536 dimensions and a few thousand chunks, this is single-digit megabytes, negligible next to the database or Qdrant container it lives in.
|
|
468
|
+
- **Infrastructure cost is the real variable.** `:local` and `:postgresql` add no new service. `:production` (Qdrant) adds one container (~300MB RAM) or a managed-cloud free tier.
|
|
469
|
+
|
|
470
|
+
The actual cost driver is developer time: initial setup (an hour or so per preset), and occasional tuning of chunking/ranking if retrieval quality needs adjustment. Choose the preset that fits infrastructure you already run, the numbers above rarely change that decision.
|