|
| 1 | +# Neo4j VectorIO Provider Implementation Plan |
| 2 | + |
| 3 | +Goal: add a first-slice `remote::neo4j` VectorIO provider that behaves like the |
| 4 | +other OGX vector store providers and proves deterministic graph-aware retrieval |
| 5 | +without changing the public OGX API. |
| 6 | + |
| 7 | +## Review Findings |
| 8 | + |
| 9 | +- The provider must use Neo4j's async driver internally because `VectorIO` is |
| 10 | + async. The live integration test should therefore use that driver through the |
| 11 | + real adapter contract, rather than relying on the synchronous OGX client |
| 12 | + wrapper or a mocked driver. |
| 13 | +- It tried to pass graph-specific params through `EmbeddingIndex` methods, whose |
| 14 | + shared protocol does not accept backend-specific params. The corrected design |
| 15 | + performs graph expansion in the Neo4j adapter after normal query execution. |
| 16 | +- It proposed enabling shared filter and OpenAI vector-store allowlists before |
| 17 | + proving Neo4j compatibility. The first implementation should explicitly leave |
| 18 | + those broad allowlists unchanged unless the focused tests pass. |
| 19 | +- It mixed a full generated-docs rollout into the first code slice. Generated |
| 20 | + provider docs remain part of the provider change, while CI is included here |
| 21 | + because the deterministic Neo4j integration test must run against a real |
| 22 | + service in pull requests. |
| 23 | +- The deterministic graph test should not depend on a sentence-transformer query |
| 24 | + embedding matching hand-written 3D chunk embeddings. It should use fixed |
| 25 | + vectors and deterministic keyword input against a local Neo4j container while |
| 26 | + exercising `Neo4jVectorIOAdapter`; direct index calls may be used only for the |
| 27 | + focused vector-index assertion. |
| 28 | + |
| 29 | +## Scope |
| 30 | + |
| 31 | +In scope: |
| 32 | + |
| 33 | +- `Neo4jVectorIOConfig` with documented Pydantic fields and sample env config. |
| 34 | +- `remote::neo4j` registry entry and `neo4j` dependency wiring. |
| 35 | +- Provider factory, driver lifecycle, KV-backed vector-store registration, chunk |
| 36 | + insert/delete, vector search, keyword search, hybrid search, and graph expansion. |
| 37 | +- Unit tests for provider-local config, lifecycle, filter translation, hybrid |
| 38 | + scoring, and graph-expansion ranking. These tests must not connect to Neo4j. |
| 39 | +- A focused optional integration test that runs only when `NEO4J_URI` is set and |
| 40 | + exercises the real adapter, index creation, chunk persistence, retrieval, and |
| 41 | + deterministic relationships against a live Neo4j process. It compares the |
| 42 | + same query with graph retrieval disabled and enabled to demonstrate the |
| 43 | + additional related chunk returned by GraphRAG. |
| 44 | + |
| 45 | +Out of scope for this slice: |
| 46 | + |
| 47 | +- New public API fields. |
| 48 | +- Router or shared `EmbeddingIndex` protocol changes. |
| 49 | +- Adding Neo4j to broad filter/OpenAI vector-store allowlists. |
| 50 | +- Broader provider allowlist changes and unrelated CI suites. |
| 51 | +- The VectorIO GitHub Actions matrix is in scope: it starts Neo4j 2025.10, |
| 52 | + waits for Bolt readiness, passes connection variables to the Neo4j job, and |
| 53 | + uploads Neo4j logs on failure. |
| 54 | + |
| 55 | +## Design |
| 56 | + |
| 57 | +### Provider Config |
| 58 | + |
| 59 | +Create `src/ogx/providers/remote/vector_io/neo4j/config.py` with |
| 60 | +`Neo4jVectorIOConfig`. |
| 61 | + |
| 62 | +Required fields: |
| 63 | + |
| 64 | +- `uri`, default `bolt://localhost:7687` |
| 65 | +- `user`, default `neo4j` |
| 66 | +- `password`, optional `SecretStr` |
| 67 | +- `database`, default `neo4j` |
| 68 | +- `index_prefix`, default `ogx` |
| 69 | +- `graph_retrieval_enabled`, default `False` |
| 70 | +- `graph_expansion_depth`, default `1`, allowed range `1..3` |
| 71 | +- `graph_max_neighbors`, default `10` |
| 72 | +- `graph_expansion_weight`, default `0.15` |
| 73 | +- `graph_relationship_types`, optional list of Neo4j relationship types |
| 74 | +- `persistence: KVStoreReference` |
| 75 | +- `metadata_store: SqlStoreReference | None` |
| 76 | + |
| 77 | +`sample_run_config()` should use `${env.NEO4J_*}` templates and persist under |
| 78 | +`vector_io::neo4j`. |
| 79 | + |
| 80 | +### Adapter |
| 81 | + |
| 82 | +Create `src/ogx/providers/remote/vector_io/neo4j/__init__.py` and |
| 83 | +`src/ogx/providers/remote/vector_io/neo4j/neo4j.py`. |
| 84 | + |
| 85 | +The adapter should: |
| 86 | + |
| 87 | +- Extend `OpenAIVectorStoreMixin`, `VectorIO`, and `VectorStoresProtocolPrivate`. |
| 88 | +- Use `AsyncGraphDatabase.driver()` and verify connectivity during initialize. |
| 89 | +- Initialize `kvstore` from `config.persistence`. |
| 90 | +- Initialize optional `metadata_store` using `authorized_sqlstore()`. |
| 91 | +- Hydrate persisted vector stores from KV under `vector_stores:neo4j:v1::`. |
| 92 | +- Implement `register_vector_store`, `unregister_vector_store`, |
| 93 | + `_get_and_cache_vector_store_index`, `insert_chunks`, `query_chunks`, and |
| 94 | + `delete_chunks`. |
| 95 | +- Call `initialize_openai_vector_stores()` after KV/metadata store setup. |
| 96 | +- Close the driver on shutdown. |
| 97 | + |
| 98 | +`query_chunks()` should call the shared `VectorStoreWithIndex.query_chunks()`, |
| 99 | +then call a provider-local graph expansion helper when either provider config or |
| 100 | +request params enable graph retrieval. |
| 101 | + |
| 102 | +### Neo4jIndex |
| 103 | + |
| 104 | +`Neo4jIndex` should implement `EmbeddingIndex`. |
| 105 | + |
| 106 | +Node model: |
| 107 | + |
| 108 | +- One per-vector-store chunk label, derived from a sanitized vector-store id. |
| 109 | +- Properties: |
| 110 | + - `chunk_id` |
| 111 | + - `content` |
| 112 | + - `embedding` |
| 113 | +- `chunk_content`, stored as a JSON string |
| 114 | +- safe metadata mirror properties named `metadata_<key>` for primitive metadata |
| 115 | + values used by filters |
| 116 | +- `vector_store_id` |
| 117 | + |
| 118 | +Chunk writes are upserts keyed by `chunk_id`. They replace all provider-owned |
| 119 | +node properties, including mirrored metadata, so re-ingesting a chunk cannot |
| 120 | +leave stale filterable metadata behind. Replacing properties must preserve |
| 121 | +relationships attached to the chunk node. |
| 122 | + |
| 123 | +Indexes: |
| 124 | + |
| 125 | +- Neo4j uniqueness constraint on `chunk_id` for each vector-store label. |
| 126 | +- Neo4j vector index on `embedding`. |
| 127 | +- Neo4j full-text index on `content`. |
| 128 | + |
| 129 | +Queries: |
| 130 | + |
| 131 | +- Vector: `CALL db.index.vector.queryNodes(...)`. |
| 132 | +- Keyword: `CALL db.index.fulltext.queryNodes(...)`. |
| 133 | +- Hybrid: query vector and keyword separately, combine using |
| 134 | + `WeightedInMemoryAggregator.combine_search_results()`. |
| 135 | + |
| 136 | +### Filters |
| 137 | + |
| 138 | +Support metadata filters in the first slice because broad VectorIO tests already |
| 139 | +exercise provider filters. Implement `_translate_filters()` for: |
| 140 | + |
| 141 | +- comparison: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` |
| 142 | +- compound: `and`, `or` |
| 143 | + |
| 144 | +Validate metadata keys with a conservative identifier regexp and use Cypher |
| 145 | +parameters for all values. Translate metadata filters against the mirrored |
| 146 | +`metadata_<key>` properties, not a nested `metadata` map, because Neo4j node |
| 147 | +properties cannot be nested maps. Unsupported filters should raise |
| 148 | +`NotImplementedError` with a `Failed to ...` message. |
| 149 | + |
| 150 | +### Graph Expansion |
| 151 | + |
| 152 | +Graph expansion remains provider-local and runs after normal retrieval. |
| 153 | + |
| 154 | +Inputs: |
| 155 | + |
| 156 | +- seed chunks and scores from normal query execution |
| 157 | +- `graph_retrieval_enabled` |
| 158 | +- `graph_expansion_depth` |
| 159 | +- `graph_max_neighbors` |
| 160 | +- `graph_expansion_weight` |
| 161 | +- optional `graph_relationship_types` |
| 162 | + |
| 163 | +Behavior: |
| 164 | + |
| 165 | +- If disabled or there are no seed chunks, return the original response. |
| 166 | +- Traverse from seed chunk nodes to neighboring chunk nodes through graph |
| 167 | + relationship paths. |
| 168 | +- Do not return the seed chunk as its own neighbor. |
| 169 | +- Score neighbor chunks as `max(seed_score) * graph_expansion_weight`. |
| 170 | +- Merge seed and neighbor chunks, preserve the highest score per chunk id, and |
| 171 | + return top `max_chunks`. |
| 172 | + |
| 173 | +For the first implementation, graph fixtures can manually create relationships |
| 174 | +from chunk nodes to `(:Entity)` nodes in tests. |
| 175 | + |
| 176 | +### Registration And Dependencies |
| 177 | + |
| 178 | +- Add `RemoteProviderSpec` for `remote::neo4j` in |
| 179 | + `src/ogx/providers/registry/vector_io.py`. |
| 180 | +- Add `neo4j` to `starter`, `test`, and `type_checking` dependency surfaces in |
| 181 | + `pyproject.toml`. |
| 182 | +- Do not update generated provider docs or CI matrix until the focused unit and |
| 183 | + optional local integration tests pass. |
| 184 | + |
| 185 | +## TDD Checklist |
| 186 | + |
| 187 | +1. Add failing unit tests in `tests/unit/providers/vector_io/test_neo4j.py`. |
| 188 | +2. Verify they fail because the module/provider is missing. |
| 189 | +3. Implement config, factory, and lifecycle. |
| 190 | +4. Verify unit tests pass. |
| 191 | +5. Add failing unit tests for filter translation and graph expansion helpers. |
| 192 | +6. Implement filters and graph expansion. |
| 193 | +7. Verify unit tests pass. |
| 194 | +8. Add optional focused integration test in |
| 195 | + `tests/integration/vector_io/test_neo4j_graph_retrieval.py`. This test must |
| 196 | + use `Neo4jVectorIOAdapter`, not only a direct in-process `Neo4jIndex`, so the |
| 197 | + registered provider path is covered against a real Neo4j process. |
| 198 | +9. Run it with local Neo4j when available: |
| 199 | + |
| 200 | +```bash |
| 201 | +NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=ogxpassword \ |
| 202 | +uv run pytest tests/integration/vector_io/test_neo4j_graph_retrieval.py -q |
| 203 | +``` |
| 204 | + |
| 205 | +1. Run focused style/pre-commit checks for changed files. |
| 206 | + |
| 207 | +## Local Neo4j |
| 208 | + |
| 209 | +Known-good local container: |
| 210 | + |
| 211 | +```bash |
| 212 | +docker run -d \ |
| 213 | + --name ogx-neo4j \ |
| 214 | + -p 7474:7474 \ |
| 215 | + -p 7687:7687 \ |
| 216 | + -e NEO4J_AUTH=neo4j/ogxpassword \ |
| 217 | + -v ogx-neo4j-data:/data \ |
| 218 | + neo4j:2025.10 |
| 219 | +``` |
| 220 | + |
| 221 | +Readiness check: |
| 222 | + |
| 223 | +```bash |
| 224 | +for i in {1..60}; do |
| 225 | + if docker exec ogx-neo4j cypher-shell -u neo4j -p ogxpassword 'RETURN 1 AS ok'; then |
| 226 | + exit 0 |
| 227 | + fi |
| 228 | + sleep 2 |
| 229 | +done |
| 230 | +docker logs ogx-neo4j |
| 231 | +exit 1 |
| 232 | +``` |
0 commit comments