polyvector-search 0.1.0__tar.gz
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.
- polyvector_search-0.1.0/LICENSE +21 -0
- polyvector_search-0.1.0/PKG-INFO +373 -0
- polyvector_search-0.1.0/README.md +345 -0
- polyvector_search-0.1.0/polyvector/__init__.py +29 -0
- polyvector_search-0.1.0/polyvector/adapters/__init__.py +5 -0
- polyvector_search-0.1.0/polyvector/adapters/sqlalchemy.py +20 -0
- polyvector_search-0.1.0/polyvector/core/__init__.py +13 -0
- polyvector_search-0.1.0/polyvector/core/compiler.py +185 -0
- polyvector_search-0.1.0/polyvector/core/models.py +77 -0
- polyvector_search-0.1.0/polyvector/embeddings/__init__.py +7 -0
- polyvector_search-0.1.0/polyvector/embeddings/base.py +17 -0
- polyvector_search-0.1.0/polyvector/embeddings/local_onnx.py +29 -0
- polyvector_search-0.1.0/polyvector/index.py +232 -0
- polyvector_search-0.1.0/polyvector/pool.py +63 -0
- polyvector_search-0.1.0/polyvector_search.egg-info/PKG-INFO +373 -0
- polyvector_search-0.1.0/polyvector_search.egg-info/SOURCES.txt +20 -0
- polyvector_search-0.1.0/polyvector_search.egg-info/dependency_links.txt +1 -0
- polyvector_search-0.1.0/polyvector_search.egg-info/requires.txt +9 -0
- polyvector_search-0.1.0/polyvector_search.egg-info/top_level.txt +1 -0
- polyvector_search-0.1.0/pyproject.toml +40 -0
- polyvector_search-0.1.0/setup.cfg +4 -0
- polyvector_search-0.1.0/tests/test_polyvector_library.py +145 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PolyVector Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyvector-search
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-performance in-engine multi-field hybrid search for PostgreSQL using vertical HNSW vectors and native BM25.
|
|
5
|
+
Author: PolyVector Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: search,vector,postgres,pgvector,bm25,hybrid-search,embeddings,onnx
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Database
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
21
|
+
Requires-Dist: fastembed>=0.2.0
|
|
22
|
+
Requires-Dist: numpy>=1.20.0
|
|
23
|
+
Provides-Extra: sqlalchemy
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "sqlalchemy"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# โก PolyVector
|
|
30
|
+
|
|
31
|
+
> **High-Performance, In-Engine Multi-Field Hybrid Search for PostgreSQL.**
|
|
32
|
+
> Native vector embeddings + length-normalized BM25 full-text search combined in a **single unified SQL query**โ100% local, zero API tokens, and universal ORM compatibility.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ๐ Comprehensive Benchmark (100,000 Documents)
|
|
37
|
+
|
|
38
|
+
PolyVector was rigorously evaluated against **Elasticsearch 8.x** and **PostgreSQL Native Full-Text Search (FTS)** on an un-cached catalog of **100,000 records** using CPU-only inference (`BAAI/bge-small-en-v1.5`, 384 dimensions).
|
|
39
|
+
|
|
40
|
+
> **๐ Scalability Note:** All numbers below reflect the verified **100,000-document benchmark**. A final stress test on **1,000,000 records (1M)** is scheduled following the library stabilization to verify continuous linear scaling and memory bounds.
|
|
41
|
+
|
|
42
|
+
### 1. Performance & Relevance Summary Table
|
|
43
|
+
|
|
44
|
+
| Metric | PostgreSQL Native FTS | Elasticsearch 8.x (Hybrid) | **PolyVector (In-Engine Hybrid)** | Advantage of PolyVector |
|
|
45
|
+
| :--- | :---: | :---: | :---: | :--- |
|
|
46
|
+
| **Median Latency (p50)** | 51.66 ms | 132.07 ms | **40.40 ms** *(16โ25 ms pooled)* | **3.2ร faster** than Elasticsearch Hybrid |
|
|
47
|
+
| **95th Percentile (p95)** | 67.95 ms | 316.85 ms | **82.29 ms** | **3.8ร faster** than Elasticsearch Hybrid |
|
|
48
|
+
| **99th Percentile (p99)** | 75.98 ms | 351.79 ms | **124.83 ms** | Predictable tail latencies under heavy load |
|
|
49
|
+
| **Throughput (QPS)** | 19.50 QPS | 6.82 QPS | **20.17 QPS** | **2.9ร higher throughput** than Elasticsearch |
|
|
50
|
+
| **RAM Consumed (Engine)** | ~256 MB *(shared buffers)* | ~2.5 GB โ 4.0 GB *(JVM heap)* | **~350 MB** *(Postgres RAM)* | **~85% less memory** than Elasticsearch |
|
|
51
|
+
| **Index Size on Disk** | 4.75 MB | 767.01 MB | **298.87 MB** | **61% smaller** storage footprint than ES |
|
|
52
|
+
| **Infrastructure Stack** | 1 Engine (PostgreSQL) | 2 Engines + Sync Pipeline | **1 Engine (PostgreSQL)** | Zero sync pipelines, zero split-brain risk |
|
|
53
|
+
| **NDCG@10** *(Ranking Quality)* | 0.6696 | 0.9696 | **0.9253** | **+38.2%** over Postgres FTS, near-parity with ES |
|
|
54
|
+
| **MRR** *(First Result Accuracy)* | 0.7143 | 1.0000 | **1.0000** | Perfect first-hit accuracy (same as ES) |
|
|
55
|
+
| **Precision@10** | 0.6349 | 0.9286 | **0.8714** | **+37.2%** over Postgres FTS |
|
|
56
|
+
| **Recall@100** *(Coverage)* | 0.4471 | 0.9471 | **0.8343** | **+86.6%** over Postgres FTS |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### ๐ Benchmark Terms Explained
|
|
61
|
+
|
|
62
|
+
- **p50 / p95 / p99 Latency:** 50%, 95%, and 99% of user search queries completed in under this time.
|
|
63
|
+
- **QPS (Queries Per Second):** Sustained query throughput executed per single engine core.
|
|
64
|
+
- **RAM Occupied:** Active operating memory allocated by the search service to keep indexes and caches hot.
|
|
65
|
+
- **Storage on Disk:** Combined physical footprint of the inverted index, vector graph (HNSW), and metadata.
|
|
66
|
+
- **Infrastructure Stack:** Number of independent stateful systems required to operate search in production.
|
|
67
|
+
- **NDCG@10 (Normalized Discounted Cumulative Gain at 10):** Measures ranking quality. Rewards algorithms that place the most relevant items at the top of the list.
|
|
68
|
+
- **MRR (Mean Reciprocal Rank):** Measures how quickly a user finds their first relevant hit ($1 / \text{rank}$). A score of `1.0000` means rank #1 hit on all queries.
|
|
69
|
+
- **Precision@10:** The proportion of top-10 returned documents that are genuinely relevant.
|
|
70
|
+
- **Recall@100:** The percentage of truly relevant documents retrieved within the top 100 candidates.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## ๐ก Why PolyVector?
|
|
75
|
+
|
|
76
|
+
1. **Zero API Tokens / 100% Local ONNX Inference:**
|
|
77
|
+
PolyVector runs local embeddings directly on your application CPU via ONNX Runtime (`fastembed` in Python, `@xenova/transformers` in JS/TS). Your data never leaves your VPC, you incur \$0 in third-party API fees, and you never hit rate limits.
|
|
78
|
+
2. **Partial HNSW Vector Indexes:**
|
|
79
|
+
Instead of maintaining a massive single vector table, PolyVector partitions vector graphs by field (`WHERE field_name = 'name'`). Each graph stays compact, cache-resident in RAM, and searches significantly faster.
|
|
80
|
+
3. **In-Engine Single Query Fusion:**
|
|
81
|
+
Eliminates application-side Reciprocal Rank Fusion (RRF) round-trips. PolyVector compiles candidate branches (`UNION ALL`) and computes weighted linear score fusion (`SUM(partial_score) GROUP BY entity_id`) directly inside PostgreSQL.
|
|
82
|
+
4. **Length-Normalized BM25:**
|
|
83
|
+
Standard full-text search often biases long documents. PolyVector incorporates `ts_rank_cd(fts_doc, query, 32)` to eliminate document length bias, matching Elasticsearch's BM25 accuracy.
|
|
84
|
+
5. **Universal ORM & Driver Agnostic:**
|
|
85
|
+
Outputs pure, ANSI-compliant SQL with parameterized bindings (`:param` or `%(param)s`), working natively with Prisma, Drizzle, SQLAlchemy, Django, or raw database drivers.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## ๐ Python Guide
|
|
90
|
+
|
|
91
|
+
### 1. Installation
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Core package (psycopg2 + fastembed)
|
|
95
|
+
pip install polyvector
|
|
96
|
+
|
|
97
|
+
# Optional: with SQLAlchemy adapter
|
|
98
|
+
pip install "polyvector[sqlalchemy]"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 2. Complete Setup & Search Flow
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from polyvector import PolyVectorIndex, ConnectionPool
|
|
105
|
+
|
|
106
|
+
# 1. Initialize persistent connection pool
|
|
107
|
+
pool = ConnectionPool(
|
|
108
|
+
config="postgresql://postgres:postgres@localhost:5432/my_database",
|
|
109
|
+
minconn=2,
|
|
110
|
+
maxconn=10
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# 2. Define the index configuration
|
|
114
|
+
index = PolyVectorIndex(
|
|
115
|
+
table="products",
|
|
116
|
+
id_column="id",
|
|
117
|
+
embeddings_table="product_embeddings",
|
|
118
|
+
fields={
|
|
119
|
+
"name": {"weight": 0.45, "type": "vector"},
|
|
120
|
+
"description": {"weight": 0.20, "type": "vector"},
|
|
121
|
+
"fts_doc": {"weight": 0.35, "type": "bm25"}
|
|
122
|
+
},
|
|
123
|
+
distinct_on="name", # Deduplicate variants
|
|
124
|
+
filter_fields=["category", "price"], # Tier-1/Tier-2 filterable fields
|
|
125
|
+
connection_pool=pool
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# 3. Create tables, pgvector extensions, and partial HNSW indexes
|
|
129
|
+
index.setup()
|
|
130
|
+
|
|
131
|
+
# 4. Ingest documents (Local ONNX embeds automatically on CPU)
|
|
132
|
+
documents = [
|
|
133
|
+
{
|
|
134
|
+
"id": "item_101",
|
|
135
|
+
"name": "Men's Waterproof Trail Runner 4",
|
|
136
|
+
"description": "All-terrain running shoes with Gore-Tex lining.",
|
|
137
|
+
"category": "Footwear",
|
|
138
|
+
"price": 149.99
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
index.index(documents)
|
|
142
|
+
|
|
143
|
+
# 5. Hybrid Search with Decoupled Latency Metrics
|
|
144
|
+
response = index.search(
|
|
145
|
+
query="waterproof trail runner",
|
|
146
|
+
filters={"price": {"lte": 160.0}},
|
|
147
|
+
limit=5
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
print(f"Total: {response.total_latency_ms:.2f}ms "
|
|
151
|
+
f"[Model: {response.embed_latency_ms:.2f}ms | DB: {response.db_latency_ms:.2f}ms]")
|
|
152
|
+
|
|
153
|
+
for item in response.items:
|
|
154
|
+
print(f"[{item.score:.4f}] ID: {item.id}")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## ๐จ JavaScript & TypeScript Guide (`@polyvector/core`)
|
|
160
|
+
|
|
161
|
+
PolyVector is designed with identical architecture for Node.js, Bun, and Deno using `@xenova/transformers` for local ONNX CPU inference.
|
|
162
|
+
|
|
163
|
+
### 1. Installation
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm install @polyvector/core
|
|
167
|
+
# or
|
|
168
|
+
pnpm add @polyvector/core
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 2. TypeScript Quickstart
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { PolyVectorIndex } from "@polyvector/core";
|
|
175
|
+
import { Pool } from "pg";
|
|
176
|
+
|
|
177
|
+
const pgPool = new Pool({
|
|
178
|
+
connectionString: "postgresql://postgres:postgres@localhost:5432/my_database"
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const index = new PolyVectorIndex({
|
|
182
|
+
table: "products",
|
|
183
|
+
idColumn: "id",
|
|
184
|
+
embeddingsTable: "product_embeddings",
|
|
185
|
+
fields: {
|
|
186
|
+
name: { weight: 0.45, type: "vector" },
|
|
187
|
+
description: { weight: 0.20, type: "vector" },
|
|
188
|
+
fts_doc: { weight: 0.35, type: "bm25" }
|
|
189
|
+
},
|
|
190
|
+
distinctOn: "name",
|
|
191
|
+
filterFields: ["category", "price"],
|
|
192
|
+
pool: pgPool
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Setup schema
|
|
196
|
+
await index.setup();
|
|
197
|
+
|
|
198
|
+
// Execute search
|
|
199
|
+
const results = await index.search("waterproof trail runner", {
|
|
200
|
+
filters: { price: { lte: 160.0 } },
|
|
201
|
+
limit: 5
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
console.log(`Executed in ${results.totalLatencyMs}ms`);
|
|
205
|
+
for (const item of results.items) {
|
|
206
|
+
console.log(`[${item.score.toFixed(4)}] ID: ${item.id}`);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## ๐ Universal ORM Integration Examples
|
|
213
|
+
|
|
214
|
+
Because PolyVector compiles **pure, ANSI-standard parameterized SQL**, you are never trapped by ORM limitations or proprietary query builders.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Example 1: Prisma (TypeScript)
|
|
219
|
+
|
|
220
|
+
Use PolyVector's `compileQuery()` to generate the parameterized SQL, then execute it directly via Prisma's `$queryRawUnsafe`:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { PrismaClient } from "@prisma/client";
|
|
224
|
+
import { PolyVectorIndex } from "@polyvector/core";
|
|
225
|
+
|
|
226
|
+
const prisma = new PrismaClient();
|
|
227
|
+
const index = new PolyVectorIndex({
|
|
228
|
+
table: "Product",
|
|
229
|
+
idColumn: "id",
|
|
230
|
+
fields: {
|
|
231
|
+
name: { weight: 0.5, type: "vector" },
|
|
232
|
+
description: { weight: 0.5, type: "bm25" }
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
async function searchProducts(searchTerm: string, maxPrice: number) {
|
|
237
|
+
// 1. Compile pure SQL & bind parameters (:vec, :q, :limit)
|
|
238
|
+
const { sql, params } = await index.compileQuery(searchTerm, {
|
|
239
|
+
filters: { price: { lte: maxPrice } },
|
|
240
|
+
limit: 10,
|
|
241
|
+
paramStyle: "named"
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 2. Execute via Prisma's native raw interface
|
|
245
|
+
// Prisma maps results seamlessly into your type-safe model structure
|
|
246
|
+
const rankedItems = await prisma.$queryRawUnsafe<Array<{ id: string; score: number }>>(
|
|
247
|
+
sql,
|
|
248
|
+
params
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return rankedItems;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### Example 2: Drizzle ORM (TypeScript)
|
|
258
|
+
|
|
259
|
+
Drizzle provides native raw execution via `sql.raw` or `db.execute`:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
263
|
+
import { sql } from "drizzle-orm";
|
|
264
|
+
import { PolyVectorIndex } from "@polyvector/core";
|
|
265
|
+
|
|
266
|
+
const db = drizzle(process.env.DATABASE_URL!);
|
|
267
|
+
const index = new PolyVectorIndex({
|
|
268
|
+
table: "products",
|
|
269
|
+
fields: {
|
|
270
|
+
title: { weight: 0.6, type: "vector" },
|
|
271
|
+
content: { weight: 0.4, type: "bm25" }
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
async function searchWithDrizzle(queryText: string) {
|
|
276
|
+
// Compile SQL with named parameters
|
|
277
|
+
const { sql: querySql, params } = await index.compileQuery(queryText, {
|
|
278
|
+
limit: 10,
|
|
279
|
+
paramStyle: "named"
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Execute using Drizzle's sql template
|
|
283
|
+
const results = await db.execute(sql.raw(querySql, params));
|
|
284
|
+
return results.rows;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### Example 3: Universal Pure SQL Execution (Any ORM / Raw Driver)
|
|
291
|
+
|
|
292
|
+
Whether you use **SQLAlchemy**, **Django ORM**, **TypeORM**, or raw **psycopg3 / pg**, PolyVector gives you the exact query string and parameter format required:
|
|
293
|
+
|
|
294
|
+
#### A. SQLAlchemy / SQLModel (Python)
|
|
295
|
+
```python
|
|
296
|
+
from sqlalchemy import create_engine
|
|
297
|
+
from sqlalchemy.orm import Session
|
|
298
|
+
from polyvector import PolyVectorIndex, SQLAlchemyAdapter
|
|
299
|
+
|
|
300
|
+
engine = create_engine("postgresql+psycopg2://user:pass@localhost:5432/db")
|
|
301
|
+
|
|
302
|
+
with Session(engine) as session:
|
|
303
|
+
results = SQLAlchemyAdapter.search(
|
|
304
|
+
index=index,
|
|
305
|
+
session=session,
|
|
306
|
+
query="trail runner",
|
|
307
|
+
filters={"category": "Footwear"},
|
|
308
|
+
limit=10
|
|
309
|
+
)
|
|
310
|
+
for row in results:
|
|
311
|
+
print(row.id, row.score)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### B. Django ORM (Python)
|
|
315
|
+
```python
|
|
316
|
+
from myapp.models import Product
|
|
317
|
+
|
|
318
|
+
sql, params = index.compile_query("trail runner", limit=10, param_style="pyformat")
|
|
319
|
+
|
|
320
|
+
# Hydrate directly into Django model instances
|
|
321
|
+
products = Product.objects.raw(sql, params)
|
|
322
|
+
for p in products:
|
|
323
|
+
print(p.name, p.price)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### C. Raw `node-postgres` (`pg` driver)
|
|
327
|
+
```javascript
|
|
328
|
+
const { sql, params } = await index.compileQuery("trail runner", { limit: 10 });
|
|
329
|
+
const { rows } = await client.query(sql, Object.values(params));
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
> **Why this works universally:**
|
|
333
|
+
> PolyVector uses standard ANSI `CAST(:vec AS vector)` and `CAST(:meta AS jsonb)` instead of dialect-specific operators (`::vector`), ensuring that PostgreSQL parses parameters correctly across **every single ORM and database driver**.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## ๐ ๏ธ Advanced Customization: `text_extractor`
|
|
338
|
+
|
|
339
|
+
When indexing data, you often want to combine multiple columns or parse nested JSON before generating vectors. Use `text_extractor`:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
index = PolyVectorIndex(
|
|
343
|
+
table="books",
|
|
344
|
+
fields={
|
|
345
|
+
# Enriches the vector with author and year context
|
|
346
|
+
"rich_content": {
|
|
347
|
+
"weight": 0.6,
|
|
348
|
+
"extractor": lambda r: f"{r['title']} by {r['author']} ({r['published_year']})"
|
|
349
|
+
},
|
|
350
|
+
# Extracts nested JSON specification
|
|
351
|
+
"specs_vector": {
|
|
352
|
+
"weight": 0.4,
|
|
353
|
+
"extractor": lambda r: f"{r['specs']['format']} edition, {r['specs']['pages']} pages"
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## ๐งช Testing
|
|
362
|
+
|
|
363
|
+
Run the test suite against a live PostgreSQL instance:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
pytest -v tests/test_polyvector_library.py
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## ๐ License
|
|
372
|
+
|
|
373
|
+
MIT License. Free for commercial and open-source use.
|