Packages

Production adapters and pipelines for PortfolioCore. Vector stores, graph stores, embedders, Broadway pipelines, and advanced RAG strategies.

Current section

6 Versions

Jump to

Compare versions

53 files changed
+10649 additions
-291 deletions
  @@ -7,6 +7,292 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7 7
8 8 ## [Unreleased]
9 9
10 + ## [0.3.1] - 2025-12-30
11 +
12 + ### Added
13 +
14 + #### Collection Selection & Self-Correction
15 + - `PortfolioIndex.Adapters.CollectionSelector.LLM` - LLM-based collection routing
16 + - Routes queries to relevant collections based on descriptions
17 + - Returns JSON with selected collections and reasoning
18 + - Custom prompt support via `:prompt` option
19 + - `PortfolioIndex.Adapters.CollectionSelector.RuleBased` - Rule-based collection routing
20 + - Keyword matching with configurable boost factors
21 + - Deterministic routing without LLM calls
22 + - Useful for testing and predictable behavior
23 + - `PortfolioIndex.RAG.SelfCorrectingSearch` - Search with sufficiency evaluation and query rewriting
24 + - Evaluates if results are sufficient for answering the question
25 + - Rewrites query and retries if insufficient
26 + - Configurable max iterations and custom prompts
27 + - Tracks correction history in context
28 + - `PortfolioIndex.RAG.SelfCorrectingAnswer` - Answer generation with grounding evaluation
29 + - Evaluates if answer is grounded in provided context
30 + - Identifies ungrounded claims and generates corrections
31 + - Configurable max corrections and grounding threshold
32 + - Tracks correction history for debugging
33 + - `PortfolioIndex.RAG.Reranker` - Pipeline-integrated reranking utilities
34 + - `rerank/2` - Rerank context results with threshold filtering
35 + - `rerank_chunks/3` - Direct chunk reranking
36 + - `deduplicate/2` - Remove duplicate chunks by id or content
37 + - Score tracking in context
38 +
39 + #### Enhanced Agentic Strategy
40 + - `PortfolioIndex.RAG.Strategies.Agentic.execute_pipeline/2` - Full pipeline execution with all enhancements
41 + - Query rewriting, expansion, decomposition
42 + - Collection selection and routing
43 + - Self-correcting search with sufficiency evaluation
44 + - Reranking with threshold filtering
45 + - Self-correcting answer with grounding evaluation
46 + - `PortfolioIndex.RAG.Strategies.Agentic.with_context/2` - Pipeline execution with Context struct
47 + - Enables functional composition with pipe operator
48 + - Configurable step skipping via `:skip` option
49 + - Returns full Context with all intermediate results
50 +
51 + #### Telemetry Standardization
52 + - `PortfolioIndex.Telemetry.Logger` - Human-readable telemetry logger with one-line attach
53 + - Text and JSON output formats
54 + - Component-level filtering (embedder, llm, rag, vector_store, evaluation)
55 + - Duration formatting (ms, s)
56 + - Context-aware metadata display
57 + - `PortfolioIndex.Telemetry.LLM` - LLM-specific telemetry with token tracking
58 + - `span/2` for wrapping LLM calls with detailed metadata
59 + - `estimate_tokens/1` for token count estimation
60 + - `extract_usage/1` for parsing provider-specific usage data
61 + - `PortfolioIndex.Telemetry.Embedder` - Embedder telemetry utilities
62 + - `span/2` for single embedding operations
63 + - `batch_span/2` for batch embedding operations
64 + - `PortfolioIndex.Telemetry.RAG` - RAG pipeline step telemetry
65 + - `step_span/3` for wrapping pipeline steps (rewrite, expand, decompose, etc.)
66 + - `search_span/3` for search-specific telemetry
67 + - `rerank_span/3` for rerank-specific telemetry
68 + - `correction_event/2` for self-correction tracking
69 + - `PortfolioIndex.Telemetry.VectorStore` - Vector store operation telemetry
70 + - `search_span/2` for search operations
71 + - `insert_span/2` for single insert operations
72 + - `batch_insert_span/2` for batch insert operations
73 +
74 + #### Vector Store Enhancements
75 + - `PortfolioIndex.Adapters.VectorStore.Memory` - In-memory HNSWLib vector store
76 + - GenServer-based in-memory storage for testing and development
77 + - Uses HNSWLib for approximate nearest neighbor (ANN) search
78 + - Soft deletion support (marks as deleted without index rebuild)
79 + - Optional file-based persistence via save/load
80 + - Thread-safe for concurrent reads and writes
81 + - Configurable dimensions, max_elements, ef_construction, and m parameters
82 + - `PortfolioIndex.VectorStore.Backend` - Backend resolution with per-call override
83 + - Runtime backend switching via `:backend` option
84 + - Backend aliases: `:pgvector`, `:memory`, `:qdrant`
85 + - Module and tuple configuration support: `{Memory, store: pid}`
86 + - Unified API for search, insert, insert_batch, delete, get
87 + - `PortfolioIndex.VectorStore.IndexManager` - Index auto-creation and management
88 + - `ensure_index/2` - Create index if not exists
89 + - `index_exists?/2` - Check index existence
90 + - `index_stats/2` - Get index statistics
91 + - `rebuild_index/2` - Rebuild index after bulk inserts
92 + - `drop_index/2` - Remove index
93 + - Backend-specific options for pgvector (HNSW, IVFFlat) and memory stores
94 + - `PortfolioIndex.VectorStore.Collections` - Collection-based organization
95 + - Logical grouping of vectors via metadata filtering
96 + - `search_collection/3` - Search within specific collection
97 + - `insert_to_collection/5` - Insert with collection tag
98 + - `list_collections/1` - List all collections
99 + - `collection_stats/2` - Get collection statistics
100 + - `clear_collection/2` - Delete all vectors in collection
101 + - `PortfolioIndex.VectorStore.SoftDelete` - Soft deletion support
102 + - `soft_delete/2` - Mark item as deleted without removal
103 + - `soft_delete_where/2` - Soft delete matching items
104 + - `restore/2` - Restore soft-deleted item
105 + - `purge_deleted/2` - Permanently delete old soft-deleted items
106 + - `count_deleted/1` - Count soft-deleted items
107 + - `PortfolioIndex.VectorStore.Search` - Enhanced search with hybrid support
108 + - `similarity_search/2` - Vector search with threshold and collection filtering
109 + - `hybrid_search/3` - Combine vector and keyword search with RRF
110 + - `filter_results/2` - Post-filter by metadata
111 + - `normalize_scores/2` - Normalize across distance metrics
112 + - `deduplicate/2` - Remove duplicate results by id or content_hash
113 +
114 + #### Embedder Enhancements
115 + - `PortfolioIndex.Adapters.Embedder.OpenAI` - OpenAI text-embedding API adapter
116 + - Full implementation replacing placeholder with actual API calls
117 + - Support for text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002
118 + - Batch embedding support via native API
119 + - Telemetry instrumentation for monitoring
120 + - `PortfolioIndex.Adapters.Embedder.Bumblebee` - Local Bumblebee/Nx.Serving embeddings
121 + - HuggingFace model loading with EXLA compilation
122 + - Support for BGE, MiniLM, and other sentence-transformers
123 + - Supervision tree integration via `child_spec/1`
124 + - No API calls required - fully local inference
125 + - `PortfolioIndex.Adapters.Embedder.Function` - Custom function wrapper adapter
126 + - Wrap any function as an embedder
127 + - Optional batch function support
128 + - Useful for testing and custom integrations
129 + - `PortfolioIndex.Embedder.Registry` - Model dimension registry
130 + - Pre-configured dimensions for OpenAI, Voyage, Bumblebee, Ollama models
131 + - Runtime registration for custom models
132 + - Provider lookup by model name
133 + - `PortfolioIndex.Embedder.Config` - Unified embedder configuration
134 + - Shorthand syntax (:openai, :bumblebee, etc.)
135 + - Module and tuple configuration support
136 + - Function wrapper auto-resolution
137 + - `current/0` and `current_dimensions/0` helpers
138 + - `PortfolioIndex.Embedder.DimensionDetector` - Automatic dimension detection
139 + - Multiple detection strategies (explicit, registry, probe)
140 + - Dimension validation for embeddings
141 + - Fallback probing for unknown models
142 +
143 + #### Retrieval Evaluation System
144 + - `PortfolioIndex.Adapters.RetrievalMetrics.Standard` - Standard IR metrics adapter
145 + - Implements `PortfolioCore.Ports.RetrievalMetrics` behaviour
146 + - `recall_at_k/3` - Recall at K (fraction of relevant items retrieved)
147 + - `precision_at_k/3` - Precision at K (fraction of retrieved items relevant)
148 + - `mrr/2` - Mean Reciprocal Rank (inverse of first relevant rank)
149 + - `hit_rate_at_k/3` - Hit Rate at K (1 if any relevant item in top K)
150 + - Aggregation with mean calculation across test cases
151 + - `PortfolioIndex.Schemas.TestCase` - Ecto schema for evaluation test cases
152 + - Links questions to ground truth chunks via many-to-many
153 + - Source: `:synthetic` (LLM-generated) or `:manual`
154 + - Collection and metadata support
155 + - `PortfolioIndex.Schemas.EvaluationRun` - Ecto schema for evaluation runs
156 + - Status tracking: `:running`, `:completed`, `:failed`
157 + - Aggregate metrics and per-case results storage
158 + - Timing and configuration tracking
159 + - `PortfolioIndex.Evaluation.Generator` - LLM-powered test case generation
160 + - `generate/2` - Sample chunks and generate synthetic questions
161 + - `generate_question/2` - Generate single question from chunk content
162 + - Chunk sampling with collection/source filtering
163 + - `PortfolioIndex.Evaluation` - Main evaluation orchestrator
164 + - `run/2` - Execute evaluation against test cases
165 + - `list_test_cases/2` - List with filtering options
166 + - `create_test_case/2` - Create manual test cases
167 + - `add_ground_truth/3` - Link chunks as ground truth
168 + - `list_runs/2` - Get historical evaluation runs
169 + - `compare_runs/2` - Compare metrics between runs
170 + - Database migration for evaluation tables
171 + - `portfolio_evaluation_test_cases` with source and collection indexes
172 + - `portfolio_evaluation_test_case_chunks` join table
173 + - `portfolio_evaluation_runs` with status index
174 +
175 + #### Production Maintenance Utilities
176 + - `PortfolioIndex.Maintenance` - Production maintenance utilities (reembed, diagnostics, retry)
177 + - `reembed/2` - Re-embed all chunks or filtered subset with progress tracking
178 + - `diagnostics/1` - Get system diagnostics including counts and storage usage
179 + - `retry_failed/2` - Reset failed documents to pending for reprocessing
180 + - `cleanup_deleted/2` - Permanently remove soft-deleted documents and chunks
181 + - `verify_embeddings/1` - Verify embedding consistency across chunks
182 + - `PortfolioIndex.Maintenance.Progress` - Progress reporting for maintenance operations
183 + - `cli_reporter/1` - Prints progress to stdout with percentage
184 + - `silent_reporter/0` - No-op reporter for silent operations
185 + - `telemetry_reporter/1` - Emits telemetry events for monitoring
186 + - `build_event/4` - Create progress events from components
187 +
188 + #### Mix Tasks
189 + - `mix portfolio.install` - Installation task for new projects
190 + - Generates database migrations for collections, documents, and chunks tables
191 + - Creates pgvector extension setup with HNSW index
192 + - Prints configuration instructions and next steps
193 + - Options: `--repo`, `--dimension`, `--no-migrations`
194 + - `mix portfolio.gen.embedding_migration` - Generate migration for dimension changes
195 + - Creates migration to alter vector column dimensions
196 + - Drops and recreates HNSW index for new dimensions
197 + - Options: `--dimension` (required), `--table`, `--column`
198 +
199 + #### Document Management Schemas
200 + - `PortfolioIndex.Schemas.Collection` - Ecto schema for document collections
201 + - Groups related documents for organized retrieval and routing
202 + - Unique name constraint with metadata support
203 + - Virtual `document_count` field for aggregation
204 + - `PortfolioIndex.Schemas.Document` - Ecto schema for ingested documents with status tracking
205 + - Status enum: `:pending`, `:processing`, `:completed`, `:failed`, `:deleted`
206 + - Content hash for deduplication via `compute_hash/1`
207 + - Collection relationship for document organization
208 + - Error message tracking for failed ingestions
209 + - `PortfolioIndex.Schemas.Chunk` - Ecto schema for document chunks with pgvector embeddings
210 + - Native `Pgvector.Ecto.Vector` type for similarity search
211 + - Document relationship with cascade delete
212 + - Character offset tracking (`start_char`, `end_char`)
213 + - Token count for LLM context budgeting
214 + - `PortfolioIndex.Schemas.Queries` - Query helpers for schema operations
215 + - `get_collection_by_name/2` - Fetch collection by name
216 + - `get_or_create_collection/3` - Upsert collection
217 + - `list_documents_by_status/3` - Filter documents by processing status
218 + - `get_document_with_chunks/2` - Load document with preloaded chunks
219 + - `similarity_search/3` - pgvector cosine similarity search
220 + - `count_chunks_without_embedding/1` - Count unprocessed chunks
221 + - `get_failed_documents/2` - Fetch documents for retry
222 + - `soft_delete_document/2` - Mark document as deleted
223 + - Database migration for document management tables
224 + - `portfolio_collections` with unique name index
225 + - `portfolio_documents` with status, source_id, and content_hash indexes
226 + - `portfolio_chunks` with HNSW vector index for fast similarity search
227 +
228 + #### Query Processing Pipeline
229 + - `PortfolioIndex.RAG.Pipeline.Context` struct for pipeline state tracking
230 + - Flows through RAG pipeline accumulating intermediate results
231 + - Tracks query transformations, routing, retrieval, and generation
232 + - Supports functional composition with pipe operator
233 + - Error propagation and halt semantics
234 + - `PortfolioIndex.Adapters.QueryRewriter.LLM` - LLM-based query cleaning
235 + - Removes greetings, filler phrases, conversational noise
236 + - Preserves technical terms and entity names
237 + - Custom prompt support
238 + - `PortfolioIndex.Adapters.QueryExpander.LLM` - LLM-based query expansion
239 + - Adds synonyms and related terms for better recall
240 + - Expands abbreviations (ML -> machine learning)
241 + - Tracks added terms for debugging
242 + - `PortfolioIndex.Adapters.QueryDecomposer.LLM` - LLM-based query decomposition
243 + - Breaks complex questions into 2-4 simpler sub-questions
244 + - Enables parallel retrieval for multi-faceted queries
245 + - JSON response parsing with fallback handling
246 + - `PortfolioIndex.RAG.QueryProcessor` - unified query processing module
247 + - `rewrite/2` - Apply query rewriting to pipeline context
248 + - `expand/2` - Apply query expansion to pipeline context
249 + - `decompose/2` - Apply query decomposition to pipeline context
250 + - `process/2` - Run all processing steps with skip options
251 + - `effective_query/1` - Get best query for retrieval
252 +
253 + #### Chunker Enhancements
254 + - **Separators module** - Centralized language-specific separators for 17+ formats
255 + - Language support: Elixir, Ruby, PHP, Python, JavaScript, TypeScript, Vue, HTML
256 + - Document formats: doc, docx, epub, latex, odt, pdf, rtf
257 + - Markdown with header-aware splitting
258 + - **Config module** - NimbleOptions-based configuration validation with compile-time schema
259 + - **Pluggable `get_chunk_size` function** - Token-based chunking support across all strategies
260 + - Character, byte, word, or custom tokenizer-based sizing
261 + - Defaults to `String.length/1` for backwards compatibility
262 +
263 + #### Token Utilities
264 + - **Tokens module** - Centralized token estimation utilities
265 + - `Tokens.estimate/2` - Estimate token count from text (~4 chars/token heuristic)
266 + - `Tokens.sizer/1` - Get sizing function for token-based chunking
267 + - `Tokens.to_chars/2` - Convert token count to character count
268 + - `Tokens.from_chars/2` - Convert character count to token count
269 + - `Tokens.default_ratio/0` - Returns the default chars-per-token ratio (4)
270 +
271 + #### Chunker Config Enhancement
272 + - **`size_unit` option** - Specify `:characters` or `:tokens` for chunk sizing
273 + - `:tokens` auto-configures `get_chunk_size` with token estimation
274 + - `:characters` (default) uses `String.length/1`
275 + - Implements `PortfolioCore.Ports.Chunker` port specification for `size_unit`
276 +
277 + #### Chunker Output Enhancement
278 + - **`token_count` in metadata** - All chunkers now include estimated token count
279 + - Useful for LLM context window budgeting
280 + - Approximately `char_count / 4`
281 + - Included in: Recursive, Character, Paragraph, Sentence, Semantic chunkers
282 +
283 + ### Changed
284 +
285 + - All chunker adapters now support `:get_chunk_size` option for custom size measurement
286 + - Recursive chunker uses new Separators module for format-specific splitting
287 + - Added nimble_options dependency for robust configuration validation
288 +
289 + ### Documentation
290 +
291 + - Added technical documentation in `docs/20251230/chunker-enhancements/`
292 + - `overview.md` - Feature overview and migration guide
293 + - `separators.md` - Language separator reference
294 + - `tokenization.md` - Token-based chunking guide
295 +
10 296 ## [0.3.0] - 2025-12-28
11 297
12 298 ### Added
  @@ -81,7 +367,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
81 367
82 368 - Initial release of PortfolioIndex
83 369
84 - [Unreleased]: https://github.com/nshkrdotcom/portfolio_index/compare/v0.3.0...HEAD
370 + [Unreleased]: https://github.com/nshkrdotcom/portfolio_index/compare/v0.3.1...HEAD
371 + [0.3.1]: https://github.com/nshkrdotcom/portfolio_index/compare/v0.3.0...v0.3.1
85 372 [0.3.0]: https://github.com/nshkrdotcom/portfolio_index/compare/v0.2.0...v0.3.0
86 373 [0.2.0]: https://github.com/nshkrdotcom/portfolio_index/compare/v0.1.1...v0.2.0
87 374 [0.1.1]: https://github.com/nshkrdotcom/portfolio_index/compare/v0.1.0...v0.1.1
  @@ -75,7 +75,7 @@ Add `portfolio_index` to your list of dependencies in `mix.exs`:
75 75 ```elixir
76 76 def deps do
77 77 [
78 - {:portfolio_index, "~> 0.2.0"}
78 + {:portfolio_index, "~> 0.3.1"}
79 79 ]
80 80 end
81 81 ```
  @@ -255,7 +255,40 @@ config :boltx, Boltx,
255 255
256 256 | Adapter | Strategy | Features |
257 257 |---------|----------|----------|
258 - | `Recursive` | Recursive text splitting | Format-aware (markdown, code, plain) |
258 + | `Recursive` | Recursive text splitting | Format-aware for 17+ languages |
259 + | `Character` | Character-based | Boundary modes: word, sentence, none |
260 + | `Sentence` | Sentence-based | NLP tokenization, abbreviation handling |
261 + | `Paragraph` | Paragraph-based | Intelligent merge/split at boundaries |
262 + | `Semantic` | Embedding similarity | Groups by semantic coherence |
263 +
264 + #### Supported Formats
265 +
266 + | Category | Formats |
267 + |----------|---------|
268 + | Languages | Elixir, Ruby, PHP, Python, JavaScript, TypeScript, Vue |
269 + | Markup | Markdown, HTML, LaTeX |
270 + | Documents | doc, docx, epub, odt, pdf, rtf |
271 +
272 + #### Token-Based Chunking
273 +
274 + All chunkers support custom size measurement via `:get_chunk_size`:
275 +
276 + ```elixir
277 + # Character-based (default)
278 + Recursive.chunk(text, :elixir, %{chunk_size: 1000})
279 +
280 + # Token-based (for LLM context limits)
281 + Recursive.chunk(text, :elixir, %{
282 + chunk_size: 256,
283 + get_chunk_size: &MyTokenizer.count_tokens/1
284 + })
285 +
286 + # Byte-based (for storage limits)
287 + Recursive.chunk(text, :plain, %{
288 + chunk_size: 4096,
289 + get_chunk_size: &byte_size/1
290 + })
291 + ```
259 292
260 293 ## RAG Strategies
  @@ -1,16 +1,33 @@
1 1 {<<"links">>,
2 2 [{<<"GitHub">>,<<"https://github.com/nshkrdotcom/portfolio_index">>}]}.
3 3 {<<"name">>,<<"portfolio_index">>}.
4 - {<<"version">>,<<"0.3.0">>}.
4 + {<<"version">>,<<"0.3.1">>}.
5 5 {<<"description">>,
6 6 <<"Production adapters and pipelines for PortfolioCore. Vector stores, graph stores, embedders, Broadway pipelines, and advanced RAG strategies.">>}.
7 7 {<<"elixir">>,<<"~> 1.15">>}.
8 8 {<<"app">>,<<"portfolio_index">>}.
9 9 {<<"licenses">>,[<<"MIT">>]}.
10 10 {<<"files">>,
11 - [<<"lib">>,<<"lib/portfolio_index.ex">>,<<"lib/portfolio_index">>,
11 + [<<"lib">>,<<"lib/portfolio_index.ex">>,<<"lib/mix">>,<<"lib/mix/tasks">>,
12 + <<"lib/mix/tasks/portfolio.gen.embedding_migration.ex">>,
13 + <<"lib/mix/tasks/portfolio.install.ex">>,<<"lib/portfolio_index">>,
14 + <<"lib/portfolio_index/maintenance">>,
15 + <<"lib/portfolio_index/maintenance/progress.ex">>,
16 + <<"lib/portfolio_index/maintenance.ex">>,
12 17 <<"lib/portfolio_index/application.ex">>,
13 - <<"lib/portfolio_index/telemetry.ex">>,<<"lib/portfolio_index/adapters">>,
18 + <<"lib/portfolio_index/telemetry.ex">>,<<"lib/portfolio_index/schemas">>,
19 + <<"lib/portfolio_index/schemas/collection.ex">>,
20 + <<"lib/portfolio_index/schemas/queries.ex">>,
21 + <<"lib/portfolio_index/schemas/evaluation_run.ex">>,
22 + <<"lib/portfolio_index/schemas/document.ex">>,
23 + <<"lib/portfolio_index/schemas/chunk.ex">>,
24 + <<"lib/portfolio_index/schemas/test_case.ex">>,
25 + <<"lib/portfolio_index/adapters">>,
26 + <<"lib/portfolio_index/adapters/query_expander">>,
27 + <<"lib/portfolio_index/adapters/query_expander/llm.ex">>,
28 + <<"lib/portfolio_index/adapters/collection_selector">>,
29 + <<"lib/portfolio_index/adapters/collection_selector/rule_based.ex">>,
30 + <<"lib/portfolio_index/adapters/collection_selector/llm.ex">>,
14 31 <<"lib/portfolio_index/adapters/graph_store">>,
15 32 <<"lib/portfolio_index/adapters/graph_store/neo4j.ex">>,
16 33 <<"lib/portfolio_index/adapters/graph_store/neo4j">>,
  @@ -20,16 +37,27 @@
20 37 <<"lib/portfolio_index/adapters/graph_store/neo4j/community.ex">>,
21 38 <<"lib/portfolio_index/adapters/document_store">>,
22 39 <<"lib/portfolio_index/adapters/document_store/postgres.ex">>,
40 + <<"lib/portfolio_index/adapters/retrieval_metrics">>,
41 + <<"lib/portfolio_index/adapters/retrieval_metrics/standard.ex">>,
23 42 <<"lib/portfolio_index/adapters/reranker">>,
24 43 <<"lib/portfolio_index/adapters/reranker/passthrough.ex">>,
25 44 <<"lib/portfolio_index/adapters/reranker/llm.ex">>,
45 + <<"lib/portfolio_index/adapters/query_decomposer">>,
46 + <<"lib/portfolio_index/adapters/query_decomposer/llm.ex">>,
26 47 <<"lib/portfolio_index/adapters/chunker">>,
27 48 <<"lib/portfolio_index/adapters/chunker/sentence.ex">>,
28 49 <<"lib/portfolio_index/adapters/chunker/semantic.ex">>,
50 + <<"lib/portfolio_index/adapters/chunker/separators.ex">>,
29 51 <<"lib/portfolio_index/adapters/chunker/character.ex">>,
30 52 <<"lib/portfolio_index/adapters/chunker/recursive.ex">>,
53 + <<"lib/portfolio_index/adapters/chunker/config.ex">>,
54 + <<"lib/portfolio_index/adapters/chunker/tokens.ex">>,
31 55 <<"lib/portfolio_index/adapters/chunker/paragraph.ex">>,
56 + <<"lib/portfolio_index/adapters/query_rewriter">>,
57 + <<"lib/portfolio_index/adapters/query_rewriter/llm.ex">>,
32 58 <<"lib/portfolio_index/adapters/embedder">>,
59 + <<"lib/portfolio_index/adapters/embedder/function.ex">>,
60 + <<"lib/portfolio_index/adapters/embedder/bumblebee.ex">>,
33 61 <<"lib/portfolio_index/adapters/embedder/gemini.ex">>,
34 62 <<"lib/portfolio_index/adapters/embedder/open_ai.ex">>,
35 63 <<"lib/portfolio_index/adapters/llm">>,
  @@ -40,9 +68,20 @@
40 68 <<"lib/portfolio_index/adapters/vector_store/pgvector">>,
41 69 <<"lib/portfolio_index/adapters/vector_store/pgvector/hybrid.ex">>,
42 70 <<"lib/portfolio_index/adapters/vector_store/pgvector/fulltext.ex">>,
71 + <<"lib/portfolio_index/adapters/vector_store/memory.ex">>,
43 72 <<"lib/portfolio_index/adapters/vector_store/pgvector.ex">>,
44 73 <<"lib/portfolio_index/postgrex_types.ex">>,
45 - <<"lib/portfolio_index/graph_rag">>,
74 + <<"lib/portfolio_index/telemetry">>,
75 + <<"lib/portfolio_index/telemetry/rag.ex">>,
76 + <<"lib/portfolio_index/telemetry/embedder.ex">>,
77 + <<"lib/portfolio_index/telemetry/vector_store.ex">>,
78 + <<"lib/portfolio_index/telemetry/logger.ex">>,
79 + <<"lib/portfolio_index/telemetry/llm.ex">>,
80 + <<"lib/portfolio_index/embedder">>,
81 + <<"lib/portfolio_index/embedder/dimension_detector.ex">>,
82 + <<"lib/portfolio_index/embedder/registry.ex">>,
83 + <<"lib/portfolio_index/embedder/config.ex">>,
84 + <<"lib/portfolio_index/evaluation.ex">>,<<"lib/portfolio_index/graph_rag">>,
46 85 <<"lib/portfolio_index/graph_rag/entity_extractor.ex">>,
47 86 <<"lib/portfolio_index/graph_rag/community_detector.ex">>,
48 87 <<"lib/portfolio_index/graph_rag/community_summarizer.ex">>,
  @@ -52,13 +91,28 @@
52 91 <<"lib/portfolio_index/pipelines/producers">>,
53 92 <<"lib/portfolio_index/pipelines/producers/file_producer.ex">>,
54 93 <<"lib/portfolio_index/pipelines/producers/ets_producer.ex">>,
55 - <<"lib/portfolio_index/rag">>,<<"lib/portfolio_index/rag/strategy.ex">>,
94 + <<"lib/portfolio_index/evaluation">>,
95 + <<"lib/portfolio_index/evaluation/generator.ex">>,
96 + <<"lib/portfolio_index/rag">>,
97 + <<"lib/portfolio_index/rag/self_correcting_answer.ex">>,
98 + <<"lib/portfolio_index/rag/self_correcting_search.ex">>,
99 + <<"lib/portfolio_index/rag/strategy.ex">>,
100 + <<"lib/portfolio_index/rag/query_processor.ex">>,
56 101 <<"lib/portfolio_index/rag/adapter_resolver.ex">>,
102 + <<"lib/portfolio_index/rag/pipeline">>,
103 + <<"lib/portfolio_index/rag/pipeline/context.ex">>,
104 + <<"lib/portfolio_index/rag/reranker.ex">>,
57 105 <<"lib/portfolio_index/rag/strategies">>,
58 106 <<"lib/portfolio_index/rag/strategies/agentic.ex">>,
59 107 <<"lib/portfolio_index/rag/strategies/hybrid.ex">>,
60 108 <<"lib/portfolio_index/rag/strategies/self_rag.ex">>,
61 109 <<"lib/portfolio_index/rag/strategies/graph_rag.ex">>,
110 + <<"lib/portfolio_index/vector_store">>,
111 + <<"lib/portfolio_index/vector_store/backend.ex">>,
112 + <<"lib/portfolio_index/vector_store/collections.ex">>,
113 + <<"lib/portfolio_index/vector_store/index_manager.ex">>,
114 + <<"lib/portfolio_index/vector_store/soft_delete.ex">>,
115 + <<"lib/portfolio_index/vector_store/search.ex">>,
62 116 <<"lib/portfolio_index/repo.ex">>,<<"assets">>,
63 117 <<"assets/portfolio_index.svg">>,<<".formatter.exs">>,<<"mix.exs">>,
64 118 <<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
  @@ -66,7 +120,7 @@
66 120 [[{<<"name">>,<<"portfolio_core">>},
67 121 {<<"app">>,<<"portfolio_core">>},
68 122 {<<"optional">>,false},
69 - {<<"requirement">>,<<"~> 0.3.0">>},
123 + {<<"requirement">>,<<"~> 0.3.1">>},
70 124 {<<"repository">>,<<"hexpm">>}],
71 125 [{<<"name">>,<<"ecto_sql">>},
72 126 {<<"app">>,<<"ecto_sql">>},
  @@ -83,6 +137,11 @@
83 137 {<<"optional">>,false},
84 138 {<<"requirement">>,<<"~> 0.2">>},
85 139 {<<"repository">>,<<"hexpm">>}],
140 + [{<<"name">>,<<"hnswlib">>},
141 + {<<"app">>,<<"hnswlib">>},
142 + {<<"optional">>,false},
143 + {<<"requirement">>,<<"~> 0.1.6">>},
144 + {<<"repository">>,<<"hexpm">>}],
86 145 [{<<"name">>,<<"boltx">>},
87 146 {<<"app">>,<<"boltx">>},
88 147 {<<"optional">>,false},
  @@ -91,17 +150,17 @@
91 150 [{<<"name">>,<<"gemini_ex">>},
92 151 {<<"app">>,<<"gemini_ex">>},
93 152 {<<"optional">>,false},
94 - {<<"requirement">>,<<"~> 0.8.6">>},
153 + {<<"requirement">>,<<"~> 0.8.8">>},
95 154 {<<"repository">>,<<"hexpm">>}],
96 155 [{<<"name">>,<<"claude_agent_sdk">>},
97 156 {<<"app">>,<<"claude_agent_sdk">>},
98 157 {<<"optional">>,false},
99 - {<<"requirement">>,<<"~> 0.6.10">>},
158 + {<<"requirement">>,<<"~> 0.7.2">>},
100 159 {<<"repository">>,<<"hexpm">>}],
101 160 [{<<"name">>,<<"codex_sdk">>},
102 161 {<<"app">>,<<"codex_sdk">>},
103 162 {<<"optional">>,false},
104 - {<<"requirement">>,<<"~> 0.4.3">>},
163 + {<<"requirement">>,<<"~> 0.4.5">>},
105 164 {<<"repository">>,<<"hexpm">>}],
106 165 [{<<"name">>,<<"req">>},
107 166 {<<"app">>,<<"req">>},
  @@ -147,5 +206,10 @@
147 206 {<<"app">>,<<"jason">>},
148 207 {<<"optional">>,false},
149 208 {<<"requirement">>,<<"~> 1.4">>},
209 + {<<"repository">>,<<"hexpm">>}],
210 + [{<<"name">>,<<"nimble_options">>},
211 + {<<"app">>,<<"nimble_options">>},
212 + {<<"optional">>,false},
213 + {<<"requirement">>,<<"~> 1.1">>},
150 214 {<<"repository">>,<<"hexpm">>}]]}.
151 215 {<<"build_tools">>,[<<"mix">>]}.
  @@ -0,0 +1,136 @@
1 + defmodule Mix.Tasks.Portfolio.Gen.EmbeddingMigration do
2 + @moduledoc """
3 + Generates a migration for changing embedding dimensions.
4 +
5 + ## Usage
6 +
7 + mix portfolio.gen.embedding_migration --dimension 1536
8 +
9 + This is useful when switching embedding models with different dimensions.
10 +
11 + ## Options
12 +
13 + - `--dimension` - New embedding dimension (required)
14 + - `--table` - Table name (default: portfolio_chunks)
15 + - `--column` - Column name (default: embedding)
16 + - `--migrations-path` - Path for migrations (default: priv/repo/migrations)
17 + """
18 +
19 + use Mix.Task
20 +
21 + import Mix.Generator
22 +
23 + @shortdoc "Generate migration to change embedding dimensions"
24 +
25 + @impl Mix.Task
26 + def run(args) do
27 + {opts, _, _} =
28 + OptionParser.parse(args,
29 + strict: [
30 + dimension: :integer,
31 + table: :string,
32 + column: :string,
33 + migrations_path: :string
34 + ]
35 + )
36 +
37 + dimension = opts[:dimension]
38 + table = opts[:table] || "portfolio_chunks"
39 + column = opts[:column] || "embedding"
40 + migrations_path = opts[:migrations_path]
41 +
42 + unless dimension do
43 + Mix.raise(
44 + "--dimension option is required. Example: mix portfolio.gen.embedding_migration --dimension 1536"
45 + )
46 + end
47 +
48 + generate_migration(dimension, table, column, migrations_path)
49 + end
50 +
51 + defp generate_migration(dimension, table, column, migrations_path) do
52 + migrations_dir = migrations_path || default_migrations_path()
53 +
54 + # Ensure the directory exists
55 + File.mkdir_p!(migrations_dir)
56 +
57 + timestamp = Calendar.strftime(DateTime.utc_now(), "%Y%m%d%H%M%S")
58 + filename = "#{timestamp}_update_embedding_dimensions.exs"
59 + path = Path.join(migrations_dir, filename)
60 +
61 + content = migration_content(dimension, table, column)
62 +
63 + create_file(path, content)
64 +
65 + Mix.shell().info("""
66 +
67 + Generated migration: #{path}
68 +
69 + This migration will:
70 + 1. Drop the HNSW index on the #{column} column
71 + 2. Alter the #{column} column to #{dimension} dimensions
72 + 3. Recreate the HNSW index
73 +
74 + Run the migration with:
75 +
76 + mix ecto.migrate
77 +
78 + After migrating, re-embed all documents with:
79 +
80 + mix portfolio.reembed
81 +
82 + """)
83 + end
84 +
85 + defp default_migrations_path do
86 + Path.join(["priv", "repo", "migrations"])
87 + end
88 +
89 + defp migration_content(dimension, table, column) do
90 + """
91 + defmodule PortfolioIndex.Repo.Migrations.UpdateEmbeddingDimensions do
92 + use Ecto.Migration
93 +
94 + @table :#{table}
95 + @column :#{column}
96 + @new_dimension #{dimension}
97 + @old_dimension 384
98 +
99 + def up do
100 + # Drop the existing HNSW index
101 + execute "DROP INDEX IF EXISTS #{table}_#{column}_idx"
102 +
103 + # Alter the embedding column to new dimensions
104 + # This requires dropping and recreating the column due to pgvector constraints
105 + execute \"\"\"
106 + ALTER TABLE #{table}
107 + ALTER COLUMN #{column} TYPE vector(@new_dimension)
108 + \"\"\"
109 +
110 + # Recreate the HNSW index with the new dimensions
111 + execute \"\"\"
112 + CREATE INDEX #{table}_#{column}_idx ON #{table}
113 + USING hnsw (#{column} vector_cosine_ops)
114 + \"\"\"
115 + end
116 +
117 + def down do
118 + # Drop the new index
119 + execute "DROP INDEX IF EXISTS #{table}_#{column}_idx"
120 +
121 + # Revert to previous dimensions
122 + execute \"\"\"
123 + ALTER TABLE #{table}
124 + ALTER COLUMN #{column} TYPE vector(@old_dimension)
125 + \"\"\"
126 +
127 + # Recreate the original index
128 + execute \"\"\"
129 + CREATE INDEX #{table}_#{column}_idx ON #{table}
130 + USING hnsw (#{column} vector_cosine_ops)
131 + \"\"\"
132 + end
133 + end
134 + """
135 + end
136 + end
  @@ -0,0 +1,205 @@
1 + defmodule Mix.Tasks.Portfolio.Install do
2 + @moduledoc """
3 + Installs PortfolioIndex into a Phoenix application.
4 +
5 + ## Usage
6 +
7 + mix portfolio.install
8 +
9 + ## What it does
10 +
11 + 1. Creates required database migrations
12 + 2. Prints configuration instructions
13 + 3. Provides next steps for setup
14 +
15 + ## Options
16 +
17 + - `--repo` - Ecto repo module (default: inferred from app)
18 + - `--dimension` - Embedding vector dimension (default: 384)
19 + - `--no-migrations` - Skip migration generation
20 + """
21 +
22 + use Mix.Task
23 +
24 + import Mix.Generator
25 +
26 + @shortdoc "Install PortfolioIndex in your application"
27 +
28 + @migration_template """
29 + defmodule <%= @repo %>.Migrations.CreatePortfolioTables do
30 + use Ecto.Migration
31 +
32 + def up do
33 + execute "CREATE EXTENSION IF NOT EXISTS vector"
34 +
35 + create table(:portfolio_collections, primary_key: false) do
36 + add :id, :binary_id, primary_key: true
37 + add :name, :string, null: false
38 + add :description, :text
39 + add :metadata, :map, default: %{}
40 +
41 + timestamps(type: :utc_datetime)
42 + end
43 +
44 + create unique_index(:portfolio_collections, [:name])
45 +
46 + create table(:portfolio_documents, primary_key: false) do
47 + add :id, :binary_id, primary_key: true
48 + add :source_id, :string
49 + add :content_hash, :string
50 + add :title, :string
51 + add :source_path, :string
52 + add :metadata, :map, default: %{}
53 + add :status, :string, default: "pending"
54 + add :error_message, :text
55 + add :chunk_count, :integer, default: 0
56 + add :collection_id, references(:portfolio_collections, type: :binary_id, on_delete: :nilify_all)
57 +
58 + timestamps(type: :utc_datetime)
59 + end
60 +
61 + create index(:portfolio_documents, [:source_id])
62 + create index(:portfolio_documents, [:content_hash])
63 + create index(:portfolio_documents, [:status])
64 + create index(:portfolio_documents, [:collection_id])
65 +
66 + create table(:portfolio_chunks, primary_key: false) do
67 + add :id, :binary_id, primary_key: true
68 + add :content, :text, null: false
69 + add :embedding, :vector, size: <%= @dimension %>, null: true
70 + add :chunk_index, :integer, default: 0
71 + add :token_count, :integer
72 + add :start_char, :integer
73 + add :end_char, :integer
74 + add :metadata, :map, default: %{}
75 + add :document_id, references(:portfolio_documents, type: :binary_id, on_delete: :delete_all)
76 +
77 + timestamps(type: :utc_datetime)
78 + end
79 +
80 + create index(:portfolio_chunks, [:document_id])
81 +
82 + execute \"\"\"
83 + CREATE INDEX portfolio_chunks_embedding_idx ON portfolio_chunks
84 + USING hnsw (embedding vector_cosine_ops)
85 + \"\"\"
86 + end
87 +
88 + def down do
89 + drop table(:portfolio_chunks)
90 + drop table(:portfolio_documents)
91 + drop table(:portfolio_collections)
92 + # Note: We don't drop the vector extension as it may be used by other tables
93 + end
94 + end
95 + """
96 +
97 + @impl Mix.Task
98 + def run(args) do
99 + {opts, _, _} =
100 + OptionParser.parse(args,
101 + strict: [
102 + repo: :string,
103 + dimension: :integer,
104 + no_migrations: :boolean
105 + ]
106 + )
107 +
108 + repo = opts[:repo] || infer_repo()
109 + dimension = opts[:dimension] || 384
110 + skip_migrations = opts[:no_migrations] || false
111 +
112 + unless skip_migrations do
113 + generate_migration(repo, dimension)
114 + end
115 +
116 + print_instructions(repo, dimension)
117 + end
118 +
119 + defp generate_migration(repo, dimension) do
120 + repo_underscore =
121 + repo
122 + |> String.replace(".", "/")
123 + |> Macro.underscore()
124 + |> String.replace("/", "_")
125 +
126 + migrations_path = Path.join(["priv", repo_underscore, "migrations"])
127 + File.mkdir_p!(migrations_path)
128 +
129 + timestamp = Calendar.strftime(DateTime.utc_now(), "%Y%m%d%H%M%S")
130 + filename = "#{timestamp}_create_portfolio_tables.exs"
131 + path = Path.join(migrations_path, filename)
132 +
133 + content = EEx.eval_string(@migration_template, assigns: [repo: repo, dimension: dimension])
134 +
135 + create_file(path, content)
136 + end
137 +
138 + defp print_instructions(repo, dimension) do
139 + Mix.shell().info("""
140 +
141 + PortfolioIndex installation complete!
142 +
143 + Configuration for #{repo} with #{dimension} dimensions.
144 +
145 + == Next Steps ==
146 +
147 + 1. Run the migration:
148 +
149 + mix ecto.migrate
150 +
151 + 2. Configure pgvector types in your repo config:
152 +
153 + # config/config.exs
154 + config :my_app, #{repo},
155 + types: MyApp.PostgrexTypes
156 +
157 + 3. Create the types module (if not already present):
158 +
159 + # lib/my_app/postgrex_types.ex
160 + Postgrex.Types.define(
161 + MyApp.PostgrexTypes,
162 + [Pgvector.Extensions.Vector] ++ Ecto.Adapters.Postgres.extensions(),
163 + []
164 + )
165 +
166 + 4. Configure the embedder in your config:
167 +
168 + # config/config.exs
169 + config :portfolio_index,
170 + embedder: PortfolioIndex.Adapters.Embedder.Gemini,
171 + embedding: [
172 + default_dimensions: #{dimension}
173 + ]
174 +
175 + 5. Start using PortfolioIndex:
176 +
177 + # Ingest a document
178 + PortfolioIndex.Maintenance.diagnostics(#{repo})
179 +
180 + == Optional: Broadway Pipelines ==
181 +
182 + For async document processing, add the pipelines to your supervision tree:
183 +
184 + children = [
185 + #{repo},
186 + {PortfolioIndex.Pipelines.Ingestion, [repo: #{repo}]},
187 + {PortfolioIndex.Pipelines.Embedding, [repo: #{repo}]}
188 + ]
189 +
190 + """)
191 + end
192 +
193 + defp infer_repo do
194 + case Mix.Project.config()[:app] do
195 + nil ->
196 + "MyApp.Repo"
197 +
198 + app ->
199 + app
200 + |> to_string()
201 + |> Macro.camelize()
202 + |> Kernel.<>(".Repo")
203 + end
204 + end
205 + end
Loading more files…