From 2e9a82a27a6f424f18b545db4c990c9d590cfde1 Mon Sep 17 00:00:00 2001 From: Artemiy Vereshchinskiy Date: Mon, 1 Jun 2026 22:02:33 +0700 Subject: [PATCH 1/4] Docs update and SDK DX improvements --- .changeset/twenty-lies-jam.md | 10 + .github/workflows/helm-publish.yml | 45 + .gitignore | 2 +- .prettierignore | 1 + README.md | 171 +- charts/rushdb/Chart.yaml | 18 + charts/rushdb/README.md | 64 + charts/rushdb/templates/NOTES.txt | 26 + charts/rushdb/templates/_helpers.tpl | 112 ++ charts/rushdb/templates/deployment.yaml | 131 ++ charts/rushdb/templates/ingress.yaml | 37 + charts/rushdb/templates/neo4j-deployment.yaml | 95 + charts/rushdb/templates/neo4j-pvc.yaml | 17 + charts/rushdb/templates/neo4j-service.yaml | 21 + charts/rushdb/templates/pvc.yaml | 17 + charts/rushdb/templates/secret.yaml | 41 + charts/rushdb/templates/service.yaml | 13 + charts/rushdb/values.yaml | 146 ++ docs/docs/concepts/_category_.json | 6 - docs/docs/concepts/agent-memory-model.md | 91 - docs/docs/concepts/bring-your-own-vectors.mdx | 143 -- docs/docs/concepts/data-ingestion.mdx | 145 -- docs/docs/concepts/index.mdx | 307 --- docs/docs/concepts/labels.md | 78 - .../concepts/ontology-schema-discovery.md | 128 -- docs/docs/concepts/properties.md | 232 --- docs/docs/concepts/records.md | 205 -- docs/docs/concepts/relationships.mdx | 261 --- docs/docs/concepts/search/_category_.json | 6 - docs/docs/concepts/semantic-search.mdx | 171 -- docs/docs/concepts/storage.md | 326 ---- docs/docs/concepts/transactions.mdx | 380 ---- docs/docs/connect/agents.mdx | 74 + docs/docs/connect/index.mdx | 125 ++ docs/docs/connect/mcp.mdx | 191 ++ docs/docs/connect/sdks.mdx | 179 ++ docs/docs/connect/skills.mdx | 56 + .../configuration/environment-variables.mdx | 219 +++ .../configuration/get-api-key.md} | 6 +- docs/docs/deploy/configuration/index.mdx | 56 + docs/docs/deploy/configuration/security.mdx | 396 ++++ .../guides/connect-aura.mdx} | 32 +- .../guides}/mcp-operator-quickstart.mdx | 13 +- .../guides}/self-hosted-project-setup.mdx | 18 +- docs/docs/deploy/infrastructure/index.mdx | 59 + .../deploy/infrastructure/neo4j-and-aura.mdx | 158 ++ .../infrastructure/postgresql-sqlite.mdx | 83 + .../deploy/infrastructure/rushdb-platform.mdx | 162 ++ docs/docs/deploy/local-hosting/docker.mdx | 186 ++ .../docs/deploy/local-hosting/from-source.mdx | 130 ++ docs/docs/deploy/local-hosting/helm.mdx | 196 ++ docs/docs/deploy/local-hosting/index.mdx | 60 + .../deploy/remote-hosting/aws-gcp-azure.mdx | 80 + .../deploy/remote-hosting/digital-ocean.mdx | 88 + docs/docs/deploy/remote-hosting/index.mdx | 69 + .../deploy/remote-hosting/prerequisites.mdx | 44 + .../remote-hosting/self-hosting-rushdb.mdx} | 13 +- docs/docs/get-started/_category_.json | 6 - docs/docs/get-started/get-api-key.mdx | 89 - docs/docs/get-started/quick-tutorial.mdx | 22 +- docs/docs/index.mdx | 13 - docs/docs/learn/agent-memory/index.mdx | 114 ++ docs/docs/learn/agent-memory/quickstart.mdx | 303 +++ .../agent-memory/schema-self-awareness.mdx | 242 +++ .../discover-your-schema.mdx | 319 +++ .../learn/records-and-queries/export-data.mdx | 116 ++ .../records-and-queries/find-and-query.mdx | 785 ++++++++ .../learn/records-and-queries/import-data.mdx | 394 ++++ docs/docs/learn/records-and-queries/index.mdx | 44 + .../labeled-meta-property-graph.mdx | 172 ++ .../labels-and-properties.mdx | 272 +++ .../learn/records-and-queries/raw-queries.mdx | 138 ++ .../records-and-queries/store-records.mdx | 603 ++++++ .../records-and-queries/transactions.mdx | 211 ++ docs/docs/learn/reference/mcp/index.mdx | 46 + docs/docs/learn/reference/mcp/tools.mdx | 98 + docs/docs/learn/reference/python/RushDB.md | 193 ++ .../reference/python}/SearchQuery.md | 112 +- docs/docs/learn/reference/python/index.md | 55 + .../reference/python}/record.md | 32 + .../reference/python}/search-result.md | 3 + .../learn/reference/python/transaction.md | 130 ++ .../ai-and-vectors}/advanced-indexing.md | 33 +- .../rest-api/ai-and-vectors}/indexing.md | 45 +- .../rest-api/ai-and-vectors}/overview.md | 65 +- .../rest-api/ai-and-vectors}/search.md | 35 +- .../ai-and-vectors}/write-with-vectors.md | 5 +- .../learn/reference/rest-api/introduction.md | 31 + .../{ => learn/reference}/rest-api/labels.md | 1 + .../reference}/rest-api/properties.md | 1 + .../reference}/rest-api/raw-queries.md | 1 + .../rest-api/records/create-records.md | 3 +- .../rest-api/records/delete-records.md | 1 + .../rest-api/records/export-data.md | 15 +- .../rest-api/records/get-records.md | 19 +- .../rest-api/records/import-data.md | 19 +- .../rest-api/records/update-records.md | 10 +- .../reference}/rest-api/relationships.md | 5 + .../reference}/rest-api/transactions.md | 1 + .../reference/typescript}/DBRecord.md | 24 +- .../reference/typescript}/DBRecordInstance.md | 43 +- .../reference/typescript}/DBRecordTarget.md | 33 +- .../typescript}/DBRecordsArrayInstance.md | 39 +- .../reference/typescript}/Model.md | 116 +- .../reference/typescript}/RelationTarget.md | 51 +- .../reference/typescript}/RushDB.md | 54 +- .../reference/typescript}/SearchQuery.md | 266 +-- .../reference/typescript}/Transaction.md | 46 +- docs/docs/learn/reference/typescript/index.md | 58 + .../relationships/bulk-relationships.mdx | 227 +++ .../learn/relationships/connect-records.mdx | 621 ++++++ docs/docs/learn/relationships/index.mdx | 38 + .../relationships/suggested-patterns.mdx | 164 ++ .../search => learn/search-query}/group-by.md | 43 +- .../search-query}/pagination-order.md | 3 +- .../search-query/search-labels.md} | 9 +- .../search-query/search-query.md} | 58 +- .../search-query/select-expressions.md} | 137 +- .../search-query/where-operators.md} | 179 +- .../bring-your-own-vectors.mdx | 614 ++++++ .../learn/semantic-search/manage-indexes.mdx | 483 +++++ .../learn/semantic-search/semantic-search.mdx | 427 ++++ .../semantic-search/write-with-vectors.mdx | 505 +++++ .../agent-safe-query-planning.mdx | 25 +- .../agent-skills-with-openclaw.mdx | 50 +- .../agent-memory}/building-team-memory.mdx | 35 +- .../agent-memory}/episodic-memory.mdx | 60 +- .../tutorials/agent-memory}/memory-layer.mdx | 31 +- .../research-knowledge-graph.mdx | 95 +- .../ai-and-rag}/ai-semantic-search.mdx | 37 +- .../ai-and-rag}/byov-external-embeddings.mdx | 54 +- .../ai-and-rag}/byov-when-and-why.mdx | 5 +- .../ai-and-rag}/explainable-results.mdx | 44 +- .../tutorials/ai-and-rag}/graphrag.mdx | 94 +- .../ai-and-rag}/hybrid-retrieval.mdx | 31 +- .../tutorials/ai-and-rag}/rag-evaluation.mdx | 90 +- .../ai-and-rag}/rag-multi-source.mdx | 23 +- .../tutorials/ai-and-rag}/rag-pipeline.mdx | 7 +- .../tutorials/ai-and-rag}/rag-reranking.mdx | 68 +- .../semantic-search-multitenant.mdx | 30 +- .../choosing-relationship-types.mdx | 30 +- .../graph-modeling}/customer-360.mdx | 33 +- .../graph-modeling}/data-lineage.mdx | 19 +- .../graph-modeling}/modeling-hierarchies.mdx | 57 +- .../graph-modeling}/temporal-graphs.mdx | 7 +- .../graph-modeling}/thinking-in-graphs.mdx | 53 +- .../graph-modeling}/versioning-records.mdx | 9 +- docs/docs/{ => learn}/tutorials/index.mdx | 3 +- .../search-and-queries}/discovery-queries.mdx | 17 +- .../query-optimization.mdx | 51 +- .../reusable-search-query.mdx | 30 +- .../search-ux-patterns.mdx | 44 +- .../searchquery-advanced-patterns.mdx | 21 +- .../testing-searchquery.mdx | 7 +- .../tutorials/use-cases}/audit-trails.mdx | 7 +- .../tutorials/use-cases}/byoc-vs-managed.mdx | 81 +- .../use-cases}/compliance-retention.mdx | 9 +- .../use-cases}/incident-response.mdx | 7 +- .../use-cases}/is-rushdb-right-for-me.mdx | 7 +- .../use-cases}/supply-chain-traceability.mdx | 81 +- .../event-driven-ingestion.mdx | 7 +- .../working-with-data}/graph-backed-api.mdx | 28 +- .../working-with-data}/importing-data.mdx | 500 ++--- .../importing-from-mongodb.mdx | 87 +- .../third-party-webhook-ingestion.mdx | 9 +- docs/docs/mcp-server/_category_.json | 6 - docs/docs/mcp-server/configuration.mdx | 66 - docs/docs/mcp-server/examples.mdx | 90 - docs/docs/mcp-server/introduction.mdx | 52 - docs/docs/mcp-server/quickstart.mdx | 129 -- docs/docs/mcp-server/tools.mdx | 699 ------- docs/docs/mcp-server/troubleshooting.mdx | 84 - docs/docs/python-sdk/_category_.json | 6 - docs/docs/python-sdk/ai/_category_.json | 10 - docs/docs/python-sdk/ai/advanced-indexing.md | 229 --- docs/docs/python-sdk/ai/indexing.md | 223 --- docs/docs/python-sdk/ai/overview.md | 250 --- docs/docs/python-sdk/ai/search.md | 205 -- docs/docs/python-sdk/ai/write-with-vectors.md | 247 --- docs/docs/python-sdk/introduction.md | 83 - docs/docs/python-sdk/labels.md | 20 - docs/docs/python-sdk/properties.md | 63 - .../python-reference/_category_.json | 6 - .../python-reference/transaction.md | 339 ---- docs/docs/python-sdk/raw-queries.md | 36 - docs/docs/python-sdk/records/_category_.json | 5 - .../docs/python-sdk/records/create-records.md | 104 - .../docs/python-sdk/records/delete-records.md | 49 - docs/docs/python-sdk/records/get-records.md | 199 -- docs/docs/python-sdk/records/import-data.md | 101 - .../docs/python-sdk/records/update-records.md | 59 - docs/docs/python-sdk/relationships.md | 74 - docs/docs/python-sdk/transactions.md | 55 - docs/docs/rest-api/_category_.json | 6 - docs/docs/rest-api/ai/_category_.json | 10 - docs/docs/rest-api/introduction.md | 32 - docs/docs/rest-api/records/_category_.json | 5 - .../billing-model.md | 33 +- docs/docs/rushdb-cloud/index.mdx | 33 + .../knowledge-units.md | 49 +- .../rushdb-cloud/licensing-and-services.mdx | 64 + docs/docs/rushdb-cloud/project-isolation.mdx | 48 + docs/docs/tutorials/_category_.json | 9 - docs/docs/tutorials/local-setup.md | 300 --- docs/docs/typescript-sdk/_category_.json | 6 - docs/docs/typescript-sdk/ai/_category_.json | 10 - .../typescript-sdk/ai/advanced-indexing.md | 253 --- docs/docs/typescript-sdk/ai/indexing.md | 232 --- docs/docs/typescript-sdk/ai/overview.md | 242 --- docs/docs/typescript-sdk/ai/search.md | 203 -- .../typescript-sdk/ai/write-with-vectors.md | 322 --- docs/docs/typescript-sdk/introduction.md | 85 - docs/docs/typescript-sdk/labels.md | 27 - docs/docs/typescript-sdk/models.md | 161 -- docs/docs/typescript-sdk/properties.md | 57 - docs/docs/typescript-sdk/raw-queries.md | 42 - .../typescript-sdk/records/_category_.json | 6 - .../typescript-sdk/records/create-records.md | 148 -- .../typescript-sdk/records/delete-records.md | 63 - .../typescript-sdk/records/get-records.md | 254 --- .../typescript-sdk/records/import-data.md | 204 -- .../typescript-sdk/records/update-records.md | 103 - docs/docs/typescript-sdk/relationships.md | 132 -- docs/docs/typescript-sdk/transactions.md | 52 - .../typescript-reference/_category_.json | 6 - docs/docusaurus.config.ts | 342 ++-- docs/plugins/tutorials-data.cjs | 22 +- docs/sidebars.ts | 409 +++- docs/src/components/CopyPageButton.tsx | 100 +- docs/src/components/DeployMethodCard.tsx | 365 ++++ docs/src/components/DocsHomePage.tsx | 391 ---- docs/src/components/SidebarCommunity.tsx | 43 + docs/src/components/ThemeSwitch.tsx | 35 + docs/src/css/custom.css | 1736 +++++++++++------ docs/src/theme/ColorModeToggle/index.tsx | 11 + .../theme/ColorModeToggle/styles.module.css | 88 + docs/src/theme/DocBreadcrumbs/index.tsx | 31 +- docs/src/theme/DocItem/Layout/index.tsx | 5 + docs/src/theme/DocSidebar/Desktop/index.tsx | 125 ++ .../theme/DocSidebarItem/Category/index.tsx | 236 +++ docs/src/theme/DocSidebarItem/Link/index.tsx | 131 ++ docs/src/theme/Logo/index.tsx | 2 +- docs/src/theme/MDXComponents.tsx | 16 + .../Navbar/MobileSidebar/Header/index.tsx | 35 + .../Navbar/MobileSidebar/Layout/index.tsx | 38 + packages/javascript-sdk/src/api/api.ts | 85 + packages/javascript-sdk/src/api/index.ts | 9 + packages/javascript-sdk/src/api/types.ts | 53 +- packages/javascript-sdk/src/index.node.ts | 18 + packages/javascript-sdk/src/index.worker.ts | 18 + ...ationship-patterns.suggestions.e2e.test.ts | 20 +- packages/mcp-server/README.md | 13 +- packages/mcp-server/index.ts | 44 + packages/mcp-server/systemPrompt.ts | 2 + packages/mcp-server/tools.ts | 190 ++ .../tools/analyzeRelationshipPatterns.ts | 6 + .../tools/approveRelationshipPattern.ts | 6 + .../tools/deleteRelationshipPattern.ts | 12 + .../tools/ignoreRelationshipPattern.ts | 6 + .../tools/listRelationshipPatterns.ts | 6 + packages/skills/README.md | 30 +- packages/skills/package.json | 1 + .../skills/rushdb-domain-template/SKILL.md | 488 +++++ .../core/src/core/relationships/controller.ts | 12 +- .../validation/schemas/relations.schema.ts | 6 +- .../src/core/search/parser/parseSubQuery.ts | 5 +- 266 files changed, 17060 insertions(+), 11979 deletions(-) create mode 100644 .changeset/twenty-lies-jam.md create mode 100644 .github/workflows/helm-publish.yml create mode 100644 charts/rushdb/Chart.yaml create mode 100644 charts/rushdb/README.md create mode 100644 charts/rushdb/templates/NOTES.txt create mode 100644 charts/rushdb/templates/_helpers.tpl create mode 100644 charts/rushdb/templates/deployment.yaml create mode 100644 charts/rushdb/templates/ingress.yaml create mode 100644 charts/rushdb/templates/neo4j-deployment.yaml create mode 100644 charts/rushdb/templates/neo4j-pvc.yaml create mode 100644 charts/rushdb/templates/neo4j-service.yaml create mode 100644 charts/rushdb/templates/pvc.yaml create mode 100644 charts/rushdb/templates/secret.yaml create mode 100644 charts/rushdb/templates/service.yaml create mode 100644 charts/rushdb/values.yaml delete mode 100644 docs/docs/concepts/_category_.json delete mode 100644 docs/docs/concepts/agent-memory-model.md delete mode 100644 docs/docs/concepts/bring-your-own-vectors.mdx delete mode 100644 docs/docs/concepts/data-ingestion.mdx delete mode 100644 docs/docs/concepts/index.mdx delete mode 100644 docs/docs/concepts/labels.md delete mode 100644 docs/docs/concepts/ontology-schema-discovery.md delete mode 100644 docs/docs/concepts/properties.md delete mode 100644 docs/docs/concepts/records.md delete mode 100644 docs/docs/concepts/relationships.mdx delete mode 100644 docs/docs/concepts/search/_category_.json delete mode 100644 docs/docs/concepts/semantic-search.mdx delete mode 100644 docs/docs/concepts/storage.md delete mode 100644 docs/docs/concepts/transactions.mdx create mode 100644 docs/docs/connect/agents.mdx create mode 100644 docs/docs/connect/index.mdx create mode 100644 docs/docs/connect/mcp.mdx create mode 100644 docs/docs/connect/sdks.mdx create mode 100644 docs/docs/connect/skills.mdx create mode 100644 docs/docs/deploy/configuration/environment-variables.mdx rename docs/docs/{tutorials/configuring-dashboard.md => deploy/configuration/get-api-key.md} (88%) create mode 100644 docs/docs/deploy/configuration/index.mdx create mode 100644 docs/docs/deploy/configuration/security.mdx rename docs/docs/{tutorials/connect-aura-instance.mdx => deploy/guides/connect-aura.mdx} (81%) rename docs/docs/{tutorials => deploy/guides}/mcp-operator-quickstart.mdx (91%) rename docs/docs/{tutorials => deploy/guides}/self-hosted-project-setup.mdx (87%) create mode 100644 docs/docs/deploy/infrastructure/index.mdx create mode 100644 docs/docs/deploy/infrastructure/neo4j-and-aura.mdx create mode 100644 docs/docs/deploy/infrastructure/postgresql-sqlite.mdx create mode 100644 docs/docs/deploy/infrastructure/rushdb-platform.mdx create mode 100644 docs/docs/deploy/local-hosting/docker.mdx create mode 100644 docs/docs/deploy/local-hosting/from-source.mdx create mode 100644 docs/docs/deploy/local-hosting/helm.mdx create mode 100644 docs/docs/deploy/local-hosting/index.mdx create mode 100644 docs/docs/deploy/remote-hosting/aws-gcp-azure.mdx create mode 100644 docs/docs/deploy/remote-hosting/digital-ocean.mdx create mode 100644 docs/docs/deploy/remote-hosting/index.mdx create mode 100644 docs/docs/deploy/remote-hosting/prerequisites.mdx rename docs/docs/{tutorials/deployment.mdx => deploy/remote-hosting/self-hosting-rushdb.mdx} (94%) delete mode 100644 docs/docs/get-started/_category_.json delete mode 100644 docs/docs/get-started/get-api-key.mdx delete mode 100644 docs/docs/index.mdx create mode 100644 docs/docs/learn/agent-memory/index.mdx create mode 100644 docs/docs/learn/agent-memory/quickstart.mdx create mode 100644 docs/docs/learn/agent-memory/schema-self-awareness.mdx create mode 100644 docs/docs/learn/records-and-queries/discover-your-schema.mdx create mode 100644 docs/docs/learn/records-and-queries/export-data.mdx create mode 100644 docs/docs/learn/records-and-queries/find-and-query.mdx create mode 100644 docs/docs/learn/records-and-queries/import-data.mdx create mode 100644 docs/docs/learn/records-and-queries/index.mdx create mode 100644 docs/docs/learn/records-and-queries/labeled-meta-property-graph.mdx create mode 100644 docs/docs/learn/records-and-queries/labels-and-properties.mdx create mode 100644 docs/docs/learn/records-and-queries/raw-queries.mdx create mode 100644 docs/docs/learn/records-and-queries/store-records.mdx create mode 100644 docs/docs/learn/records-and-queries/transactions.mdx create mode 100644 docs/docs/learn/reference/mcp/index.mdx create mode 100644 docs/docs/learn/reference/mcp/tools.mdx create mode 100644 docs/docs/learn/reference/python/RushDB.md rename docs/docs/{python-sdk/python-reference => learn/reference/python}/SearchQuery.md (79%) create mode 100644 docs/docs/learn/reference/python/index.md rename docs/docs/{python-sdk/python-reference => learn/reference/python}/record.md (99%) rename docs/docs/{python-sdk/python-reference => learn/reference/python}/search-result.md (99%) create mode 100644 docs/docs/learn/reference/python/transaction.md rename docs/docs/{rest-api/ai => learn/reference/rest-api/ai-and-vectors}/advanced-indexing.md (84%) rename docs/docs/{rest-api/ai => learn/reference/rest-api/ai-and-vectors}/indexing.md (87%) rename docs/docs/{rest-api/ai => learn/reference/rest-api/ai-and-vectors}/overview.md (64%) rename docs/docs/{rest-api/ai => learn/reference/rest-api/ai-and-vectors}/search.md (76%) rename docs/docs/{rest-api/ai => learn/reference/rest-api/ai-and-vectors}/write-with-vectors.md (95%) create mode 100644 docs/docs/learn/reference/rest-api/introduction.md rename docs/docs/{ => learn/reference}/rest-api/labels.md (96%) rename docs/docs/{ => learn/reference}/rest-api/properties.md (98%) rename docs/docs/{ => learn/reference}/rest-api/raw-queries.md (97%) rename docs/docs/{ => learn/reference}/rest-api/records/create-records.md (95%) rename docs/docs/{ => learn/reference}/rest-api/records/delete-records.md (93%) rename docs/docs/{ => learn/reference}/rest-api/records/export-data.md (85%) rename docs/docs/{ => learn/reference}/rest-api/records/get-records.md (75%) rename docs/docs/{ => learn/reference}/rest-api/records/import-data.md (64%) rename docs/docs/{ => learn/reference}/rest-api/records/update-records.md (69%) rename docs/docs/{ => learn/reference}/rest-api/relationships.md (92%) rename docs/docs/{ => learn/reference}/rest-api/transactions.md (98%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/DBRecord.md (73%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/DBRecordInstance.md (79%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/DBRecordTarget.md (56%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/DBRecordsArrayInstance.md (79%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/Model.md (79%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/RelationTarget.md (65%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/RushDB.md (66%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/SearchQuery.md (71%) rename docs/docs/{typescript-sdk/typescript-reference => learn/reference/typescript}/Transaction.md (73%) create mode 100644 docs/docs/learn/reference/typescript/index.md create mode 100644 docs/docs/learn/relationships/bulk-relationships.mdx create mode 100644 docs/docs/learn/relationships/connect-records.mdx create mode 100644 docs/docs/learn/relationships/index.mdx create mode 100644 docs/docs/learn/relationships/suggested-patterns.mdx rename docs/docs/{concepts/search => learn/search-query}/group-by.md (84%) rename docs/docs/{concepts/search => learn/search-query}/pagination-order.md (97%) rename docs/docs/{concepts/search/labels.md => learn/search-query/search-labels.md} (97%) rename docs/docs/{concepts/search/introduction.md => learn/search-query/search-query.md} (66%) rename docs/docs/{concepts/search/select.md => learn/search-query/select-expressions.md} (73%) rename docs/docs/{concepts/search/where.md => learn/search-query/where-operators.md} (83%) create mode 100644 docs/docs/learn/semantic-search/bring-your-own-vectors.mdx create mode 100644 docs/docs/learn/semantic-search/manage-indexes.mdx create mode 100644 docs/docs/learn/semantic-search/semantic-search.mdx create mode 100644 docs/docs/learn/semantic-search/write-with-vectors.mdx rename docs/docs/{tutorials => learn/tutorials/agent-memory}/agent-safe-query-planning.mdx (92%) rename docs/docs/{tutorials => learn/tutorials/agent-memory}/agent-skills-with-openclaw.mdx (79%) rename docs/docs/{tutorials => learn/tutorials/agent-memory}/building-team-memory.mdx (91%) rename docs/docs/{tutorials => learn/tutorials/agent-memory}/episodic-memory.mdx (85%) rename docs/docs/{tutorials => learn/tutorials/agent-memory}/memory-layer.mdx (91%) rename docs/docs/{tutorials => learn/tutorials/agent-memory}/research-knowledge-graph.mdx (80%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/ai-semantic-search.mdx (90%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/byov-external-embeddings.mdx (79%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/byov-when-and-why.mdx (97%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/explainable-results.mdx (88%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/graphrag.mdx (83%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/hybrid-retrieval.mdx (91%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/rag-evaluation.mdx (80%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/rag-multi-source.mdx (95%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/rag-pipeline.mdx (97%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/rag-reranking.mdx (86%) rename docs/docs/{tutorials => learn/tutorials/ai-and-rag}/semantic-search-multitenant.mdx (95%) rename docs/docs/{tutorials => learn/tutorials/graph-modeling}/choosing-relationship-types.mdx (93%) rename docs/docs/{tutorials => learn/tutorials/graph-modeling}/customer-360.mdx (91%) rename docs/docs/{tutorials => learn/tutorials/graph-modeling}/data-lineage.mdx (95%) rename docs/docs/{tutorials => learn/tutorials/graph-modeling}/modeling-hierarchies.mdx (90%) rename docs/docs/{tutorials => learn/tutorials/graph-modeling}/temporal-graphs.mdx (97%) rename docs/docs/{tutorials => learn/tutorials/graph-modeling}/thinking-in-graphs.mdx (89%) rename docs/docs/{tutorials => learn/tutorials/graph-modeling}/versioning-records.mdx (96%) rename docs/docs/{ => learn}/tutorials/index.mdx (95%) rename docs/docs/{tutorials => learn/tutorials/search-and-queries}/discovery-queries.mdx (95%) rename docs/docs/{tutorials => learn/tutorials/search-and-queries}/query-optimization.mdx (79%) rename docs/docs/{tutorials => learn/tutorials/search-and-queries}/reusable-search-query.mdx (93%) rename docs/docs/{tutorials => learn/tutorials/search-and-queries}/search-ux-patterns.mdx (86%) rename docs/docs/{tutorials => learn/tutorials/search-and-queries}/searchquery-advanced-patterns.mdx (97%) rename docs/docs/{tutorials => learn/tutorials/search-and-queries}/testing-searchquery.mdx (96%) rename docs/docs/{tutorials => learn/tutorials/use-cases}/audit-trails.mdx (97%) rename docs/docs/{tutorials => learn/tutorials/use-cases}/byoc-vs-managed.mdx (73%) rename docs/docs/{tutorials => learn/tutorials/use-cases}/compliance-retention.mdx (97%) rename docs/docs/{tutorials => learn/tutorials/use-cases}/incident-response.mdx (98%) rename docs/docs/{tutorials => learn/tutorials/use-cases}/is-rushdb-right-for-me.mdx (96%) rename docs/docs/{tutorials => learn/tutorials/use-cases}/supply-chain-traceability.mdx (83%) rename docs/docs/{tutorials => learn/tutorials/working-with-data}/event-driven-ingestion.mdx (97%) rename docs/docs/{tutorials => learn/tutorials/working-with-data}/graph-backed-api.mdx (93%) rename docs/docs/{tutorials => learn/tutorials/working-with-data}/importing-data.mdx (74%) rename docs/docs/{tutorials => learn/tutorials/working-with-data}/importing-from-mongodb.mdx (88%) rename docs/docs/{tutorials => learn/tutorials/working-with-data}/third-party-webhook-ingestion.mdx (94%) delete mode 100644 docs/docs/mcp-server/_category_.json delete mode 100644 docs/docs/mcp-server/configuration.mdx delete mode 100644 docs/docs/mcp-server/examples.mdx delete mode 100644 docs/docs/mcp-server/introduction.mdx delete mode 100644 docs/docs/mcp-server/quickstart.mdx delete mode 100644 docs/docs/mcp-server/tools.mdx delete mode 100644 docs/docs/mcp-server/troubleshooting.mdx delete mode 100644 docs/docs/python-sdk/_category_.json delete mode 100644 docs/docs/python-sdk/ai/_category_.json delete mode 100644 docs/docs/python-sdk/ai/advanced-indexing.md delete mode 100644 docs/docs/python-sdk/ai/indexing.md delete mode 100644 docs/docs/python-sdk/ai/overview.md delete mode 100644 docs/docs/python-sdk/ai/search.md delete mode 100644 docs/docs/python-sdk/ai/write-with-vectors.md delete mode 100644 docs/docs/python-sdk/introduction.md delete mode 100644 docs/docs/python-sdk/labels.md delete mode 100644 docs/docs/python-sdk/properties.md delete mode 100644 docs/docs/python-sdk/python-reference/_category_.json delete mode 100644 docs/docs/python-sdk/python-reference/transaction.md delete mode 100644 docs/docs/python-sdk/raw-queries.md delete mode 100644 docs/docs/python-sdk/records/_category_.json delete mode 100644 docs/docs/python-sdk/records/create-records.md delete mode 100644 docs/docs/python-sdk/records/delete-records.md delete mode 100644 docs/docs/python-sdk/records/get-records.md delete mode 100644 docs/docs/python-sdk/records/import-data.md delete mode 100644 docs/docs/python-sdk/records/update-records.md delete mode 100644 docs/docs/python-sdk/relationships.md delete mode 100644 docs/docs/python-sdk/transactions.md delete mode 100644 docs/docs/rest-api/_category_.json delete mode 100644 docs/docs/rest-api/ai/_category_.json delete mode 100644 docs/docs/rest-api/introduction.md delete mode 100644 docs/docs/rest-api/records/_category_.json rename docs/docs/{concepts => rushdb-cloud}/billing-model.md (53%) create mode 100644 docs/docs/rushdb-cloud/index.mdx rename docs/docs/{concepts => rushdb-cloud}/knowledge-units.md (68%) create mode 100644 docs/docs/rushdb-cloud/licensing-and-services.mdx create mode 100644 docs/docs/rushdb-cloud/project-isolation.mdx delete mode 100644 docs/docs/tutorials/_category_.json delete mode 100644 docs/docs/tutorials/local-setup.md delete mode 100644 docs/docs/typescript-sdk/_category_.json delete mode 100644 docs/docs/typescript-sdk/ai/_category_.json delete mode 100644 docs/docs/typescript-sdk/ai/advanced-indexing.md delete mode 100644 docs/docs/typescript-sdk/ai/indexing.md delete mode 100644 docs/docs/typescript-sdk/ai/overview.md delete mode 100644 docs/docs/typescript-sdk/ai/search.md delete mode 100644 docs/docs/typescript-sdk/ai/write-with-vectors.md delete mode 100644 docs/docs/typescript-sdk/introduction.md delete mode 100644 docs/docs/typescript-sdk/labels.md delete mode 100644 docs/docs/typescript-sdk/models.md delete mode 100644 docs/docs/typescript-sdk/properties.md delete mode 100644 docs/docs/typescript-sdk/raw-queries.md delete mode 100644 docs/docs/typescript-sdk/records/_category_.json delete mode 100644 docs/docs/typescript-sdk/records/create-records.md delete mode 100644 docs/docs/typescript-sdk/records/delete-records.md delete mode 100644 docs/docs/typescript-sdk/records/get-records.md delete mode 100644 docs/docs/typescript-sdk/records/import-data.md delete mode 100644 docs/docs/typescript-sdk/records/update-records.md delete mode 100644 docs/docs/typescript-sdk/relationships.md delete mode 100644 docs/docs/typescript-sdk/transactions.md delete mode 100644 docs/docs/typescript-sdk/typescript-reference/_category_.json create mode 100644 docs/src/components/DeployMethodCard.tsx delete mode 100644 docs/src/components/DocsHomePage.tsx create mode 100644 docs/src/components/SidebarCommunity.tsx create mode 100644 docs/src/components/ThemeSwitch.tsx create mode 100644 docs/src/theme/ColorModeToggle/index.tsx create mode 100644 docs/src/theme/ColorModeToggle/styles.module.css create mode 100644 docs/src/theme/DocSidebar/Desktop/index.tsx create mode 100644 docs/src/theme/DocSidebarItem/Category/index.tsx create mode 100644 docs/src/theme/DocSidebarItem/Link/index.tsx create mode 100644 docs/src/theme/MDXComponents.tsx create mode 100644 docs/src/theme/Navbar/MobileSidebar/Header/index.tsx create mode 100644 docs/src/theme/Navbar/MobileSidebar/Layout/index.tsx create mode 100644 packages/mcp-server/tools/analyzeRelationshipPatterns.ts create mode 100644 packages/mcp-server/tools/approveRelationshipPattern.ts create mode 100644 packages/mcp-server/tools/deleteRelationshipPattern.ts create mode 100644 packages/mcp-server/tools/ignoreRelationshipPattern.ts create mode 100644 packages/mcp-server/tools/listRelationshipPatterns.ts create mode 100644 packages/skills/rushdb-domain-template/SKILL.md diff --git a/.changeset/twenty-lies-jam.md b/.changeset/twenty-lies-jam.md new file mode 100644 index 00000000..6fa1a2ac --- /dev/null +++ b/.changeset/twenty-lies-jam.md @@ -0,0 +1,10 @@ +--- +'@rushdb/javascript-sdk': minor +'@rushdb/mcp-server': minor +'@rushdb/skills': minor +'rushdb-core': minor +'rushdb-docs': minor +'rushdb-dashboard': minor +--- + +Docs update and SDK DX improvements diff --git a/.github/workflows/helm-publish.yml b/.github/workflows/helm-publish.yml new file mode 100644 index 00000000..c9866add --- /dev/null +++ b/.github/workflows/helm-publish.yml @@ -0,0 +1,45 @@ +name: Publish Helm Chart + +on: + push: + branches: + - main + paths: + - 'charts/**' + - '.github/workflows/helm-publish.yml' + # Also allow manual runs + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Package and push to GHCR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.17.0 + + - name: Log in to GHCR + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \ + --username ${{ github.actor }} \ + --password-stdin + + - name: Package chart + run: helm package charts/rushdb --destination /tmp/charts + + - name: Push chart to GHCR + run: helm push /tmp/charts/rushdb-*.tgz oci://ghcr.io/rush-db/charts + + - name: Log out + if: always() + run: helm registry logout ghcr.io diff --git a/.gitignore b/.gitignore index 7b5677f2..ae4483d1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ coverage .rollup.cache cjs esm -packages/javascript-sdk/types \ No newline at end of file +packages/javascript-sdk/types diff --git a/.prettierignore b/.prettierignore index 30509cdd..7e4d576d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ pnpm-lock.yaml **/build **/node_modules **/coverage +charts/**/templates/** diff --git a/README.md b/README.md index 9f3ceea5..093bf122 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,15 @@ ### The memory layer for AI agents and apps. -Push any JSON. Get graph relationships and vector search — automatically. -No schema. No pipeline. No glue code. +Push any JSON. Your agent gets graph relationships, semantic search, and a live queryable schema — automatically. +No pipeline. No separate stores. No schema planning. [![GitHub Stars](https://img.shields.io/github/stars/rush-db/rushdb?style=social)](https://github.com/rush-db/rushdb) [![Follow on X](https://img.shields.io/twitter/follow/rushdb?style=social)](https://x.com/RushDatabase) [![NPM Version](https://img.shields.io/npm/v/%40rushdb%2Fjavascript-sdk?label=npm)](https://www.npmjs.com/package/@rushdb/javascript-sdk) [![PyPI Version](https://img.shields.io/pypi/v/rushdb?label=pypi)](https://pypi.org/project/rushdb/) [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](packages/javascript-sdk/LICENSE) +[![CI](https://img.shields.io/github/actions/workflow/status/rush-db/rushdb/ci.yml?label=tests)](https://github.com/rush-db/rushdb/actions) [🌐 Website](https://rushdb.com) • [📖 Documentation](https://docs.rushdb.com) • [☁️ Cloud](https://app.rushdb.com) • [🔍 Examples](https://github.com/rush-db/examples) @@ -27,18 +28,23 @@ Your agent needs memory. The standard answer is three databases: Redis for key-v RushDB replaces all three. Push JSON once. Query it with graph traversal, semantic search, or both — in one call. -| Without RushDB | With RushDB | -|---|---| -| Redis + Pinecone + Neo4j + glue code | One API | -| Design schema → write migrations → repeat | Push any shape, no schema required | -| Separate embedding pipeline | Managed embeddings, server-side | -| Hand-craft relationship edges | Auto-detected from your data structure | +| Without RushDB | With RushDB | +| ----------------------------------------- | -------------------------------------- | +| Redis + Pinecone + Neo4j + glue code | One API | +| Design schema → write migrations → repeat | Push any shape, no schema required | +| Separate embedding pipeline | Managed embeddings, server-side | +| Hand-craft relationship edges | Auto-detected from your data structure | --- ## Quick start -Get an API key at [app.rushdb.com](https://app.rushdb.com), then: +Two paths depending on your setup: + +- **Cloud** — Managed, free tier, running in 30 seconds. [Get API key →](https://app.rushdb.com) +- **Self-host** — Docker + your own Neo4j instance. [Jump to Self-hosting →](#self-hosting) + +### Cloud path ```bash npm install @rushdb/javascript-sdk @@ -64,8 +70,8 @@ await db.records.create({ session_id: 'sess-001', action: 'summarized', topic: 'Q4 results', - output: summaryText, - }, + output: summaryText + } }) // Recall by meaning — graph filter + semantic search in one call @@ -74,7 +80,7 @@ const memories = await db.ai.search({ propertyName: 'output', query: 'what did we decide about Q4?', where: { agent_id: 'agent-42' }, - limit: 10, + limit: 10 }) ``` @@ -114,13 +120,17 @@ await db.records.importJson({ label: 'COMPANY', payload: { name: 'Acme Corp', - DEPARTMENT: [{ - name: 'Engineering', - EMPLOYEE: [{ - name: 'Alice', - role: 'Staff Engineer', - }] - }] + DEPARTMENT: [ + { + name: 'Engineering', + EMPLOYEE: [ + { + name: 'Alice', + role: 'Staff Engineer' + } + ] + } + ] } }) @@ -129,8 +139,8 @@ const engineers = await db.records.find({ labels: ['EMPLOYEE'], where: { role: { $contains: 'Engineer' }, - DEPARTMENT: { COMPANY: { name: 'Acme Corp' } }, - }, + DEPARTMENT: { COMPANY: { name: 'Acme Corp' } } + } }) ``` @@ -162,35 +172,34 @@ Place this in your Claude Desktop, Cursor, or Windsurf MCP config. The agent can ## What's in the box -| Capability | What it means | -|---|---| -| **Managed embeddings** | Index any string property once — every write is auto-embedded server-side | -| **Graph + vector in one query** | Semantic similarity and relationship traversal compose in a single call | -| **Zero schema** | Push any JSON shape. RushDB infers types, creates properties, links records | -| **ACID transactions** | Concurrent agents don't corrupt shared memory. Neo4j under the hood | -| **Self-describing graph** | Agents enumerate labels, properties, and value ranges to orient themselves | -| **MCP-native** | Full MCP server with discovery-first query prompt built in | -| **Agent Skills** | Installable `@rushdb/skills` package — teach any skills-compatible agent to query, model, and remember with RushDB in one command | -| **Unified query API** | One JSON shape for graph, vector, aggregation, and introspection | -| **Self-host or cloud** | Docker + your Neo4j, or managed cloud. Full data ownership | +| Capability | What it means | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| **Managed embeddings** | Index any string property once — every write is auto-embedded server-side | +| **Graph + vector in one query** | Semantic similarity and relationship traversal compose in a single call | +| **Zero schema** | Push any JSON shape. RushDB infers types, creates properties, links records | +| **ACID transactions** | Concurrent agents don't corrupt shared memory. Neo4j under the hood | +| **Self-describing graph** | Agents enumerate labels, properties, and value ranges to orient themselves | +| **MCP-native** | Full MCP server with discovery-first query prompt built in | +| **Agent Skills** | Installable `@rushdb/skills` package — teach any skills-compatible agent to query, model, and remember with RushDB in one command | +| **Unified query API** | One JSON shape for graph, vector, aggregation, and introspection | +| **Self-host or cloud** | Docker + your Neo4j, or managed cloud. Full data ownership | --- ## Use cases -**Agent memory** — Persist tool outputs, conversation history, and reasoning chains. Graph auto-links sessions, entities, and actions without manual relationship modeling. - -**RAG with context** — Go beyond flat chunk retrieval. Filter by relationships, rank by semantic similarity, aggregate — one query. - -**Schema-free apps** — Push raw data now, refine later. No migration churn. Labels and properties auto-discovered for dynamic UIs and AI agents. - -**Connected data products** — Product catalogs, knowledge bases, fraud graphs, biotech entities: traverse, filter, and aggregate without separate systems. +| Use case | What RushDB replaces | Key API | +| --------------------------- | ------------------------------- | ------------------------------------------------------------- | +| **Agent memory** | Redis + vector store + graph DB | `db.ai.search({ query, where: { agent_id } })` | +| **RAG with context** | Flat vector store | `db.records.find({ where, labels })` + relationship traversal | +| **Schema-free apps** | Postgres + migrations + ETL | `db.records.importJson(nestedJson)` | +| **Connected data products** | Multiple joined services | `db.records.find({ labels, where: { SOME_LABEL: { ... } } })` | --- ## Self-hosting -Requires Neo4j 2026.01.4+ with APOC and Graph Data Science plugins. +> **Self-host path** — run RushDB on your own infrastructure. Requires Neo4j 2026.01.4+ with APOC plugin. ```yaml # docker-compose.yml @@ -199,7 +208,7 @@ services: rushdb: image: rushdb/platform ports: - - "3000:3000" + - '3000:3000' environment: - NEO4J_URL=neo4j+s://your-instance.neo4j.io - NEO4J_USERNAME=neo4j @@ -212,15 +221,15 @@ services:
Full environment variables -| Name | Description | Required | Default | -|------|-------------|----------|---------| -| `NEO4J_URL` | Neo4j connection URL | yes | — | -| `NEO4J_USERNAME` | Neo4j username | yes | neo4j | -| `NEO4J_PASSWORD` | Neo4j password | yes | — | -| `RUSHDB_AES_256_ENCRYPTION_KEY` | 32-char key for API token encryption | yes (prod) | — | -| `RUSHDB_PORT` | HTTP port | no | 3000 | -| `RUSHDB_LOGIN` | Admin login | no | admin | -| `RUSHDB_PASSWORD` | Admin password | no | password | +| Name | Description | Required | Default | +| ------------------------------- | ------------------------------------ | ---------- | -------- | +| `NEO4J_URL` | Neo4j connection URL | yes | — | +| `NEO4J_USERNAME` | Neo4j username | yes | neo4j | +| `NEO4J_PASSWORD` | Neo4j password | yes | — | +| `RUSHDB_AES_256_ENCRYPTION_KEY` | 32-char key for API token encryption | yes (prod) | — | +| `RUSHDB_PORT` | HTTP port | no | 3000 | +| `RUSHDB_LOGIN` | Admin login | no | admin | +| `RUSHDB_PASSWORD` | Admin password | no | password |
@@ -236,7 +245,7 @@ services: neo4j: condition: service_healthy ports: - - "3000:3000" + - '3000:3000' environment: - NEO4J_URL=bolt://neo4j - NEO4J_USERNAME=neo4j @@ -244,13 +253,13 @@ services: neo4j: image: neo4j:2026.01.4 healthcheck: - test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider localhost:7474 || exit 1"] + test: ['CMD-SHELL', 'wget --no-verbose --tries=1 --spider localhost:7474 || exit 1'] interval: 5s retries: 30 start_period: 10s ports: - - "7474:7474" - - "7687:7687" + - '7474:7474' + - '7687:7687' environment: - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes - NEO4J_AUTH=neo4j/password @@ -272,6 +281,7 @@ rushdb update-password admin@example.com newsecurepassword456 RushDB uses a **Labeled Meta Property Graph (LMPG)** model. Properties are elevated to first-class graph nodes ("HyperProperties") — not just key-value pairs attached to records. This means: + - **Auto-detected relationships** — records sharing properties get linked without hand-crafting edges - **Schema introspection** — agents can enumerate labels, property types, value ranges, and relationship topology in one query - **Soft constraints** — type cohesion scoring, cardinality tracking, and vector dimension enforcement without rigid upfront schemas @@ -287,36 +297,51 @@ One SearchQuery retrieves multiple perspectives simultaneously (records + proper ## Documentation -| Topic | Link | -|-------|------| -| Quick Tutorial | https://docs.rushdb.com/get-started/quick-tutorial | +| Topic | Link | +| ------------------------ | -------------------------------------------------------------- | +| Quick Tutorial | https://docs.rushdb.com/get-started/quick-tutorial | | Vector / Semantic Search | https://docs.rushdb.com/concepts/search/where#vector-operators | -| Filtering & Traversal | https://docs.rushdb.com/concepts/search/where | -| Grouping & Aggregations | https://docs.rushdb.com/concepts/search/group-by | -| TypeScript SDK | https://docs.rushdb.com/typescript-sdk/introduction | -| Python SDK | https://docs.rushdb.com/python-sdk/introduction | -| REST API | https://docs.rushdb.com/rest-api/introduction | -| MCP Server | packages/mcp-server/README.md | -| Agent Skills | packages/skills/README.md | +| Filtering & Traversal | https://docs.rushdb.com/concepts/search/where | +| Grouping & Aggregations | https://docs.rushdb.com/concepts/search/group-by | +| TypeScript SDK | https://docs.rushdb.com/typescript-sdk/introduction | +| Python SDK | https://docs.rushdb.com/python-sdk/introduction | +| REST API | https://docs.rushdb.com/rest-api/introduction | +| MCP Server | packages/mcp-server/README.md | +| Agent Skills | packages/skills/README.md | + +--- + +## When not to use RushDB + +- You need sub-millisecond latency at very high write throughput — RushDB is built on Neo4j, which prioritises consistency and query expressiveness over raw write speed. +- You only need flat key-value storage with no relationships or semantic search — a simpler store will be lighter. +- You need a rigid relational schema enforced at the database level — RushDB is deliberately schema-free. --- ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs welcome. +```bash +git clone https://github.com/rush-db/rushdb.git +cd rushdb +pnpm install +pnpm test +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines. Issues and PRs welcome. --- ## License -| Path | License | -|------|---------| -| platform/core | Elastic License 2.0 | -| platform/dashboard | Elastic License 2.0 | -| docs | Apache 2.0 | -| website | Apache 2.0 | -| packages/javascript-sdk | Apache 2.0 | -| packages/mcp-server | Apache 2.0 | +| Path | License | +| ----------------------- | ------------------- | +| platform/core | Elastic License 2.0 | +| platform/dashboard | Elastic License 2.0 | +| docs | Apache 2.0 | +| website | Apache 2.0 | +| packages/javascript-sdk | Apache 2.0 | +| packages/mcp-server | Apache 2.0 | --- diff --git a/charts/rushdb/Chart.yaml b/charts/rushdb/Chart.yaml new file mode 100644 index 00000000..5985b022 --- /dev/null +++ b/charts/rushdb/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: rushdb +description: RushDB — developer-friendly graph database platform +type: application +version: 0.1.0 +appVersion: '2.2.0' +keywords: + - rushdb + - graph + - database + - neo4j +home: https://rushdb.com +sources: + - https://github.com/rush-db/rushdb +maintainers: + - name: RushDB Team + url: https://rushdb.com +icon: https://raw.githubusercontent.com/rush-db/rushdb/main/rushdb-logo.svg diff --git a/charts/rushdb/README.md b/charts/rushdb/README.md new file mode 100644 index 00000000..c0db7cb6 --- /dev/null +++ b/charts/rushdb/README.md @@ -0,0 +1,64 @@ +# RushDB Helm Chart + +[RushDB](https://rushdb.com) is a developer-friendly graph database platform built on Neo4j. + +## TL;DR + +```bash +helm install rushdb oci://ghcr.io/rush-db/charts/rushdb \ + --set rushdb.encryptionKey="$(openssl rand -hex 16)" +``` + +## Prerequisites + +- Kubernetes 1.27+ +- Helm 3.12+ + +## Installing the chart + +```bash +# With bundled Neo4j (default) +helm install rushdb oci://ghcr.io/rush-db/charts/rushdb \ + --set rushdb.encryptionKey="32SymbolStringHere00000000000000" \ + --set rushdb.adminLogin=admin \ + --set rushdb.adminPassword=changeme + +# Against an existing Neo4j +helm install rushdb oci://ghcr.io/rush-db/charts/rushdb \ + --set rushdb.encryptionKey="32SymbolStringHere00000000000000" \ + --set neo4j.enabled=false \ + --set rushdb.neo4jUrl=bolt://my-neo4j:7687 \ + --set rushdb.neo4jPassword=password +``` + +## Configuration + +See [values.yaml](./values.yaml) for the full reference. + +| Key | Description | Default | +| ------------------------- | ------------------------------------------- | --------------- | +| `rushdb.encryptionKey` | **Required.** AES-256 key, exactly 32 chars | `""` | +| `rushdb.adminLogin` | Admin username | `admin` | +| `rushdb.adminPassword` | Admin password | `changeme` | +| `rushdb.sqlDbType` | `sqlite` or `postgres` | `sqlite` | +| `rushdb.sqlDbUrl` | PostgreSQL DSN (when `sqlDbType=postgres`) | `""` | +| `rushdb.replicaCount` | Pod replicas (use 1 with sqlite) | `1` | +| `rushdb.ingress.enabled` | Create an Ingress resource | `false` | +| `neo4j.enabled` | Deploy bundled Neo4j | `true` | +| `neo4j.password` | Neo4j password | `neo4jpassword` | +| `neo4j.persistence.size` | Neo4j data PVC size | `10Gi` | +| `rushdb.persistence.size` | SQLite data PVC size | `1Gi` | + +## Upgrading + +```bash +helm upgrade rushdb oci://ghcr.io/rush-db/charts/rushdb -f values.yaml +``` + +## Uninstalling + +```bash +helm uninstall rushdb +# PVCs are retained; delete them manually if desired: +kubectl delete pvc -l app.kubernetes.io/instance=rushdb +``` diff --git a/charts/rushdb/templates/NOTES.txt b/charts/rushdb/templates/NOTES.txt new file mode 100644 index 00000000..b60680e0 --- /dev/null +++ b/charts/rushdb/templates/NOTES.txt @@ -0,0 +1,26 @@ +RushDB {{ .Chart.AppVersion }} has been installed. + +Get the application URL: +{{- if .Values.rushdb.ingress.enabled }} +{{- range .Values.rushdb.ingress.hosts }} + http{{ if $.Values.rushdb.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else if contains "NodePort" .Values.rushdb.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "rushdb.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.rushdb.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "rushdb.fullname" . }} +{{- else }} + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "rushdb.fullname" . }} 3000:{{ .Values.rushdb.service.port }} + Then open http://localhost:3000 +{{- end }} + +{{- if .Values.neo4j.enabled }} + +Bundled Neo4j is enabled. It may take 60–90 seconds for Neo4j to initialize before RushDB can connect. +{{- end }} + +⚠ IMPORTANT: Keep a safe copy of your encryptionKey value. + Changing it after first install will break all existing API tokens. diff --git a/charts/rushdb/templates/_helpers.tpl b/charts/rushdb/templates/_helpers.tpl new file mode 100644 index 00000000..3783b6d7 --- /dev/null +++ b/charts/rushdb/templates/_helpers.tpl @@ -0,0 +1,112 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "rushdb.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this. +*/}} +{{- define "rushdb.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Fully qualified name for bundled Neo4j resources. +*/}} +{{- define "rushdb.neo4j.fullname" -}} +{{- printf "%s-neo4j" (include "rushdb.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create chart label. +*/}} +{{- define "rushdb.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels — RushDB platform. +*/}} +{{- define "rushdb.labels" -}} +helm.sh/chart: {{ include "rushdb.chart" . }} +{{ include "rushdb.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels — RushDB platform. +*/}} +{{- define "rushdb.selectorLabels" -}} +app.kubernetes.io/name: {{ include "rushdb.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Common labels — bundled Neo4j. +*/}} +{{- define "rushdb.neo4j.labels" -}} +helm.sh/chart: {{ include "rushdb.chart" . }} +{{ include "rushdb.neo4j.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels — bundled Neo4j. +*/}} +{{- define "rushdb.neo4j.selectorLabels" -}} +app.kubernetes.io/name: {{ include "rushdb.neo4j.fullname" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Resolve the Neo4j bolt URL for RushDB. +When the bundled Neo4j is enabled, auto-wire the service DNS. +Otherwise, use the user-supplied URL. +*/}} +{{- define "rushdb.neo4jUrl" -}} +{{- if .Values.neo4j.enabled }} +{{- printf "bolt://%s:%d" (include "rushdb.neo4j.fullname" .) (int .Values.neo4j.service.boltPort) }} +{{- else }} +{{- required "rushdb.neo4jUrl is required when neo4j.enabled=false" .Values.rushdb.neo4jUrl }} +{{- end }} +{{- end }} + +{{/* +Resolve the Neo4j password for RushDB. +*/}} +{{- define "rushdb.neo4jPassword" -}} +{{- if .Values.neo4j.enabled }} +{{- .Values.neo4j.password }} +{{- else }} +{{- .Values.rushdb.neo4jPassword }} +{{- end }} +{{- end }} + +{{/* +Resolve the Neo4j username for RushDB. +*/}} +{{- define "rushdb.neo4jUsername" -}} +{{- if .Values.neo4j.enabled -}} +neo4j +{{- else -}} +{{- default "neo4j" .Values.rushdb.neo4jUsername }} +{{- end -}} +{{- end }} diff --git a/charts/rushdb/templates/deployment.yaml b/charts/rushdb/templates/deployment.yaml new file mode 100644 index 00000000..6eb3c5ba --- /dev/null +++ b/charts/rushdb/templates/deployment.yaml @@ -0,0 +1,131 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "rushdb.fullname" . }} + labels: + {{- include "rushdb.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.rushdb.replicaCount }} + selector: + matchLabels: + {{- include "rushdb.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + # Force pod restart when the secret changes + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.rushdb.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "rushdb.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.rushdb.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.rushdb.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.rushdb.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.rushdb.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: rushdb + image: "{{ .Values.rushdb.image.repository }}:{{ .Values.rushdb.image.tag }}" + imagePullPolicy: {{ .Values.rushdb.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.rushdb.service.port }} + protocol: TCP + env: + - name: RUSHDB_PORT + value: {{ .Values.rushdb.service.port | quote }} + - name: RUSHDB_SELF_HOSTED + value: {{ .Values.rushdb.selfHosted | quote }} + - name: RUSHDB_SERVE_STATIC + value: {{ .Values.rushdb.serveStatic | quote }} + - name: SQL_DB_TYPE + value: {{ .Values.rushdb.sqlDbType | quote }} + {{- if and (eq .Values.rushdb.sqlDbType "sqlite") .Values.rushdb.persistence.enabled }} + - name: SQL_DB_PATH + value: /data/rushdb.db + {{- end }} + {{- if .Values.rushdb.dashboardUrl }} + - name: RUSHDB_DASHBOARD_URL + value: {{ .Values.rushdb.dashboardUrl | quote }} + {{- end }} + {{- if .Values.rushdb.publicUrl }} + - name: RUSHDB_PUBLIC_URL + value: {{ .Values.rushdb.publicUrl | quote }} + - name: RUSHDB_OAUTH_ISSUER + value: {{ .Values.rushdb.publicUrl | quote }} + {{- end }} + {{- if .Values.rushdb.jwtKid }} + - name: RUSHDB_JWT_KID + value: {{ .Values.rushdb.jwtKid | quote }} + {{- end }} + {{- if .Values.rushdb.embeddingBaseUrl }} + - name: RUSHDB_EMBEDDING_BASE_URL + value: {{ .Values.rushdb.embeddingBaseUrl | quote }} + {{- end }} + {{- if .Values.rushdb.embeddingModel }} + - name: RUSHDB_EMBEDDING_MODEL + value: {{ .Values.rushdb.embeddingModel | quote }} + {{- end }} + {{- if .Values.rushdb.embeddingDimensions }} + - name: RUSHDB_EMBEDDING_DIMENSIONS + value: {{ .Values.rushdb.embeddingDimensions | quote }} + {{- end }} + {{- if .Values.rushdb.llmBaseUrl }} + - name: RUSHDB_LLM_BASE_URL + value: {{ .Values.rushdb.llmBaseUrl | quote }} + {{- end }} + {{- if .Values.rushdb.llmModel }} + - name: RUSHDB_LLM_MODEL + value: {{ .Values.rushdb.llmModel | quote }} + {{- end }} + {{- range $key, $val := .Values.rushdb.extraEnv }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end }} + envFrom: + - secretRef: + name: {{ include "rushdb.fullname" . }} + {{- with .Values.rushdb.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.rushdb.resources | nindent 12 }} + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 15 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + {{- if and (eq .Values.rushdb.sqlDbType "sqlite") .Values.rushdb.persistence.enabled }} + volumeMounts: + - name: data + mountPath: /data + {{- end }} + {{- if and (eq .Values.rushdb.sqlDbType "sqlite") .Values.rushdb.persistence.enabled }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "rushdb.fullname" . }} + {{- end }} diff --git a/charts/rushdb/templates/ingress.yaml b/charts/rushdb/templates/ingress.yaml new file mode 100644 index 00000000..7279610d --- /dev/null +++ b/charts/rushdb/templates/ingress.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rushdb.ingress.enabled -}} +{{- $fullName := include "rushdb.fullname" . -}} +{{- $svcPort := .Values.rushdb.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "rushdb.labels" . | nindent 4 }} + {{- with .Values.rushdb.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.rushdb.ingress.className }} + ingressClassName: {{ .Values.rushdb.ingress.className }} + {{- end }} + {{- if .Values.rushdb.ingress.tls }} + tls: + {{- toYaml .Values.rushdb.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.rushdb.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/rushdb/templates/neo4j-deployment.yaml b/charts/rushdb/templates/neo4j-deployment.yaml new file mode 100644 index 00000000..c94afb5b --- /dev/null +++ b/charts/rushdb/templates/neo4j-deployment.yaml @@ -0,0 +1,95 @@ +{{- if .Values.neo4j.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "rushdb.neo4j.fullname" . }} + labels: + {{- include "rushdb.neo4j.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "rushdb.neo4j.selectorLabels" . | nindent 6 }} + strategy: + type: Recreate # single-instance; Recreate avoids two pods competing for the PVC + template: + metadata: + annotations: + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.neo4j.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "rushdb.neo4j.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.neo4j.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.neo4j.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.neo4j.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.neo4j.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: neo4j + image: "{{ .Values.neo4j.image.repository }}:{{ .Values.neo4j.image.tag }}" + imagePullPolicy: {{ .Values.neo4j.image.pullPolicy }} + ports: + - name: bolt + containerPort: {{ .Values.neo4j.service.boltPort }} + protocol: TCP + - name: http + containerPort: {{ .Values.neo4j.service.httpPort }} + protocol: TCP + env: + - name: NEO4J_PLUGINS + value: {{ .Values.neo4j.plugins | quote }} + - name: NEO4J_dbms_security_procedures_unrestricted + value: "apoc.*" + - name: NEO4J_dbms_security_procedures_allowlist + value: "apoc.*" + {{- range $key, $val := .Values.neo4j.extraEnv }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end }} + envFrom: + - secretRef: + name: {{ include "rushdb.neo4j.fullname" . }} + {{- with .Values.neo4j.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.neo4j.resources | nindent 12 }} + livenessProbe: + tcpSocket: + port: bolt + initialDelaySeconds: 60 + periodSeconds: 20 + failureThreshold: 5 + readinessProbe: + tcpSocket: + port: bolt + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 5 + {{- if .Values.neo4j.persistence.enabled }} + volumeMounts: + - name: data + mountPath: /data + {{- end }} + {{- if .Values.neo4j.persistence.enabled }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "rushdb.neo4j.fullname" . }} + {{- end }} +{{- end }} diff --git a/charts/rushdb/templates/neo4j-pvc.yaml b/charts/rushdb/templates/neo4j-pvc.yaml new file mode 100644 index 00000000..f4819f29 --- /dev/null +++ b/charts/rushdb/templates/neo4j-pvc.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.neo4j.enabled .Values.neo4j.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "rushdb.neo4j.fullname" . }} + labels: + {{- include "rushdb.neo4j.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.neo4j.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.neo4j.persistence.size }} + {{- if .Values.neo4j.persistence.storageClass }} + storageClassName: {{ .Values.neo4j.persistence.storageClass }} + {{- end }} +{{- end }} diff --git a/charts/rushdb/templates/neo4j-service.yaml b/charts/rushdb/templates/neo4j-service.yaml new file mode 100644 index 00000000..7798a749 --- /dev/null +++ b/charts/rushdb/templates/neo4j-service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.neo4j.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "rushdb.neo4j.fullname" . }} + labels: + {{- include "rushdb.neo4j.labels" . | nindent 4 }} +spec: + type: {{ .Values.neo4j.service.type }} + ports: + - port: {{ .Values.neo4j.service.boltPort }} + targetPort: bolt + protocol: TCP + name: bolt + - port: {{ .Values.neo4j.service.httpPort }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "rushdb.neo4j.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/rushdb/templates/pvc.yaml b/charts/rushdb/templates/pvc.yaml new file mode 100644 index 00000000..4d0c496d --- /dev/null +++ b/charts/rushdb/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if and (eq .Values.rushdb.sqlDbType "sqlite") .Values.rushdb.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "rushdb.fullname" . }} + labels: + {{- include "rushdb.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.rushdb.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.rushdb.persistence.size }} + {{- if .Values.rushdb.persistence.storageClass }} + storageClassName: {{ .Values.rushdb.persistence.storageClass }} + {{- end }} +{{- end }} diff --git a/charts/rushdb/templates/secret.yaml b/charts/rushdb/templates/secret.yaml new file mode 100644 index 00000000..6206fa8b --- /dev/null +++ b/charts/rushdb/templates/secret.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "rushdb.fullname" . }} + labels: + {{- include "rushdb.labels" . | nindent 4 }} +type: Opaque +stringData: + RUSHDB_AES_256_ENCRYPTION_KEY: {{ required "rushdb.encryptionKey is required (must be exactly 32 characters)" .Values.rushdb.encryptionKey | quote }} + RUSHDB_LOGIN: {{ .Values.rushdb.adminLogin | quote }} + RUSHDB_PASSWORD: {{ .Values.rushdb.adminPassword | quote }} + NEO4J_URL: {{ include "rushdb.neo4jUrl" . | quote }} + NEO4J_USERNAME: {{ include "rushdb.neo4jUsername" . | quote }} + NEO4J_PASSWORD: {{ include "rushdb.neo4jPassword" . | quote }} + {{- if .Values.rushdb.sqlDbUrl }} + SQL_DB_URL: {{ .Values.rushdb.sqlDbUrl | quote }} + {{- end }} + {{- if .Values.rushdb.jwtPrivateKeyBase64 }} + RUSHDB_JWT_PRIVATE_KEY_BASE64: {{ .Values.rushdb.jwtPrivateKeyBase64 | quote }} + {{- end }} + {{- if .Values.rushdb.jwtPublicKeyBase64 }} + RUSHDB_JWT_PUBLIC_KEY_BASE64: {{ .Values.rushdb.jwtPublicKeyBase64 | quote }} + {{- end }} + {{- if .Values.rushdb.embeddingApiKey }} + RUSHDB_EMBEDDING_API_KEY: {{ .Values.rushdb.embeddingApiKey | quote }} + {{- end }} + {{- if .Values.rushdb.llmApiKey }} + RUSHDB_LLM_API_KEY: {{ .Values.rushdb.llmApiKey | quote }} + {{- end }} +{{- if .Values.neo4j.enabled }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "rushdb.neo4j.fullname" . }} + labels: + {{- include "rushdb.neo4j.labels" . | nindent 4 }} +type: Opaque +stringData: + NEO4J_AUTH: {{ printf "neo4j/%s" .Values.neo4j.password | quote }} +{{- end }} diff --git a/charts/rushdb/templates/service.yaml b/charts/rushdb/templates/service.yaml new file mode 100644 index 00000000..73949529 --- /dev/null +++ b/charts/rushdb/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: { { include "rushdb.fullname" . } } + labels: { { - include "rushdb.labels" . | nindent 4 } } +spec: + type: { { .Values.rushdb.service.type } } + ports: + - port: { { .Values.rushdb.service.port } } + targetPort: http + protocol: TCP + name: http + selector: { { - include "rushdb.selectorLabels" . | nindent 4 } } diff --git a/charts/rushdb/values.yaml b/charts/rushdb/values.yaml new file mode 100644 index 00000000..0d02596e --- /dev/null +++ b/charts/rushdb/values.yaml @@ -0,0 +1,146 @@ +## RushDB Helm chart default values. +## Override these in a values.yaml file or with --set flags. + +# ── RushDB platform ─────────────────────────────────────────────────────────── +rushdb: + image: + repository: rushdb/platform + tag: latest # pin to a specific release tag in production + pullPolicy: IfNotPresent + + ## Required — must be exactly 32 characters. + ## Used for AES-256-CBC encryption of API tokens. + ## Do NOT change after first install without a migration. + encryptionKey: '' + + ## Default admin credentials + adminLogin: admin + adminPassword: changeme + + ## Serving + selfHosted: true + serveStatic: true + dashboardUrl: '' # public URL of the RushDB dashboard (used in emails etc.) + publicUrl: '' # public URL of the API, e.g. https://rushdb.example.com + + ## Neo4j connection — only used when neo4j.enabled=false + neo4jUrl: '' + neo4jUsername: neo4j + neo4jPassword: '' + + ## SQL metadata store (users, projects, tokens, OAuth) + ## "sqlite" uses a PVC for the db file; "postgres" requires sqlDbUrl. + sqlDbType: sqlite + sqlDbUrl: '' # postgresql://user:pass@host:5432/rushdb + + ## Optional RS256 JWT keys for MCP OAuth (base64-encoded PEM) + jwtPrivateKeyBase64: '' + jwtPublicKeyBase64: '' + jwtKid: '' + + ## Optional embedding / LLM integration + embeddingBaseUrl: '' + embeddingApiKey: '' + embeddingModel: '' + embeddingDimensions: '' + llmBaseUrl: '' + llmApiKey: '' + llmModel: '' + + ## Number of replicas (stateless when using postgres; keep 1 for sqlite) + replicaCount: 1 + + service: + type: ClusterIP + port: 3000 + + ingress: + enabled: false + className: '' + annotations: {} + # kubernetes.io/ingress.class: nginx + # cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: [] + # - host: rushdb.example.com + # paths: + # - path: / + # pathType: Prefix + tls: [] + # - secretName: rushdb-tls + # hosts: + # - rushdb.example.com + + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + + ## SQLite data persistence (ignored when sqlDbType=postgres) + persistence: + enabled: true + size: 1Gi + storageClass: '' # leave empty to use the cluster default + accessMode: ReadWriteOnce + + ## Extra environment variables injected verbatim into the RushDB container + extraEnv: {} + # RATE_LIMITER_REQUESTS_LIMIT: "100" + # RATE_LIMITER_TTL: "1000" + + ## Pod-level settings + podAnnotations: {} + podSecurityContext: {} + securityContext: {} + nodeSelector: {} + tolerations: [] + affinity: {} + +# ── Bundled Neo4j ───────────────────────────────────────────────────────────── +# Set enabled=false and configure rushdb.neo4jUrl to use an existing Neo4j. +neo4j: + enabled: true + + image: + repository: neo4j + tag: '2026.01.4' + pullPolicy: IfNotPresent + + ## Neo4j auth password + password: neo4jpassword + + ## APOC Core is required by RushDB. Do not remove it. + plugins: '["apoc"]' + + ## Additional Neo4j environment variables (NEO4J_* format) + extraEnv: {} + # NEO4J_dbms_memory_heap_initial__size: 512m + # NEO4J_dbms_memory_heap_max__size: 1G + + service: + type: ClusterIP + boltPort: 7687 + httpPort: 7474 + + persistence: + enabled: true + size: 10Gi + storageClass: '' + accessMode: ReadWriteOnce + + resources: + requests: + cpu: 500m + memory: 2Gi + limits: + cpu: 2000m + memory: 4Gi + + podAnnotations: {} + podSecurityContext: {} + securityContext: {} + nodeSelector: {} + tolerations: [] + affinity: {} diff --git a/docs/docs/concepts/_category_.json b/docs/docs/concepts/_category_.json deleted file mode 100644 index 2df0e82f..00000000 --- a/docs/docs/concepts/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Concepts", - "position": 3, - "collapsed": false, - "collapsible": false -} diff --git a/docs/docs/concepts/agent-memory-model.md b/docs/docs/concepts/agent-memory-model.md deleted file mode 100644 index 83d6078c..00000000 --- a/docs/docs/concepts/agent-memory-model.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Agent Memory Model - -RushDB is designed from the ground up as a **structured memory store for AI agents**. Unlike flat vector databases or document stores, RushDB gives agents three distinct memory layers — each addressing a different dimension of knowledge — and a retrieval stack that composes them at query time. - -## The Three Memory Layers - -| Layer | What it stores | RushDB primitive | -|---|---|---| -| **Episodic** | Individual facts, events, entities, and their connections | Records + Relationships | -| **Semantic** | Meaning encoded as dense vectors (embeddings) | Vector Properties + AI Indexes | -| **Structural** | Schema: what labels exist, what properties they carry, how they connect | Ontology API | - -### Episodic Memory — Records and Relationships - -Every discrete piece of knowledge is a **Record**: a typed key-value object carrying a label, properties, and a system-generated ID. Records connect to one another via **Relationships**, forming a traversable knowledge graph. An agent can store anything from a conversation turn to a product entity as a record, then retrieve it by property values, label, or graph traversal. - -→ See [Records](./records.md) and [Relationships](./relationships.mdx) - -### Semantic Memory — Vector Properties - -A subset of record properties carry dense vector representations (embeddings). Because all properties in RushDB are first-class graph nodes shared across every record with the same `(name, type)`, a vector-indexed property is simultaneously a semantic index over every record it connects to. Embeddings can be supplied by the application (Bring Your Own Vector) or generated automatically by RushDB. - -→ See [Properties — Vector Properties and Semantic Indexing](./properties.md#vector-properties-and-semantic-indexing) - -### Structural Memory — Ontology - -The **Ontology API** returns a live snapshot of the graph's schema: all labels, all properties per label with types and value ranges, and the full relationship map. An agent calls this once per session to bootstrap awareness of what is in the database — no hardcoded schema, no external documentation required. - -→ See [Ontology & Schema Discovery](./ontology-schema-discovery.md) - ---- - -## The Retrieval Stack - -A well-designed agent retrieval pipeline uses all three layers in sequence: - -```mermaid -graph TD - A["Agent"] --> B["Ontology discovery"] - B --> C["Faceted filter"] - C --> D["Semantic re-rank"] - D --> E["Structured results"] - E --> F["Agent response"] -``` - -**Step 1 — Discover the schema.** Before constructing any query, the agent calls the ontology endpoint to learn what labels, properties, and relationships exist. This prevents hallucinated field names and enables dynamic query construction. - -**Step 2 — Filter structurally.** The `where` clause narrows the candidate set by exact or range conditions on scalar properties. This is fast (index-backed) and deterministic. It is the right tool when the agent knows what it is looking for. - -**Step 3 — Re-rank semantically.** After structural filtering, a `vector.similarity` aggregation scores each candidate against the agent's query embedding. This surfaces the most *relevant* records from the structurally valid candidate set. - -**Step 4 — Return to agent.** The sorted, scored result set is returned. The agent reasons over structured records — not raw text chunks — because RushDB preserves full property context alongside the similarity score. - ---- - -## Self-Awareness Without External Documentation - -A central design goal of RushDB is that agents should be able to operate against an unknown or evolving knowledge graph **without any out-of-band schema documentation**. Two mechanisms make this possible: - -1. **`__proptypes`** — every Record carries a `__proptypes` field listing the name and type of each property it holds. This makes every record self-describing. - -2. **The Ontology API** — aggregates all `__proptypes` metadata across the project and returns it as a schema snapshot. An agent that calls `/ai/ontology/md` at the start of a session receives the full graph schema in a single, token-efficient Markdown string. - -Together, these enable a zero-configuration agentic loop: - -``` -Boot → call ontology → understand what exists in the graph - → construct SearchQuery from real labels/properties - → retrieve relevant records - → act on structured, typed results -``` - ---- - -## Composing the Retrieval Approaches - -The three retrieval approaches are not mutually exclusive — they compose: - -| Approach | When to use | Mechanism | -|---|---|---| -| **Structural only** | Known labels and property values | `where` filter | -| **Semantic with pre-filter** | Meaning-based lookup, optionally scoped by structural conditions | `db.ai.search()` with `where` | -| **Structural + semantic in one query** | Full SearchQuery features (groupBy, collect, multi-hop) alongside similarity scoring | `where` + `vector.similarity` aggregation | - -Both `db.ai.search()` and `db.records.find()` support pre-filtering before scoring — choose based on whether you need managed text embedding (`db.ai.search()`) or the full SearchQuery feature set (`db.records.find()`). - -→ See [Properties — Composing Faceted and Semantic Search](./properties.md#composing-faceted-and-semantic-search) for code examples. diff --git a/docs/docs/concepts/bring-your-own-vectors.mdx b/docs/docs/concepts/bring-your-own-vectors.mdx deleted file mode 100644 index 77e555c2..00000000 --- a/docs/docs/concepts/bring-your-own-vectors.mdx +++ /dev/null @@ -1,143 +0,0 @@ ---- -sidebar_position: 10 ---- - -# Bring Your Own Vectors (BYOV) - -By default, RushDB generates embeddings server-side when you create an **embedding index** on a string property. With **BYOV** (Bring Your Own Vectors) you compute the embeddings yourself and push them alongside your records. RushDB stores, indexes, and searches them — you stay in full control of the model and the pipeline. - -## Why BYOV? - -| Scenario | Why BYOV helps | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| Domain-specific or fine-tuned model | Use any model — a locally fine-tuned LLM, a multimodal encoder, a document-structure model — without configuring it server-side | -| Compliance / data residency | Raw text never leaves your infrastructure; only the numeric vector is sent to RushDB | -| Multimodal embeddings | Encode images, audio, or structured documents into vectors before storing them | -| Existing ML pipeline | Re-use vectors already produced by your data pipeline | -| Reproducibility | Lock embedding logic to a specific model version; no coupling to server-side model upgrades | - -## Managed vs. External - -| Aspect | Managed | External (BYOV) | -| ------------------------------- | ---------------------------------- | --------------------------------------------------------- | -| `sourceType` | `managed` (default) | `external` | -| Who generates embeddings | RushDB server | Your application | -| Search input | Natural-language `query` string | Pre-computed `queryVector` array | -| `dimensions` required on create | No — uses server default | **Yes** — must match your model | -| Initial index status | `pending` → `ready` after backfill | `awaiting_vectors` → `ready` once first vector is written | -| Backfill existing records | Automatic | Manual via `upsertVectors` or inline writes | - -Both index types can coexist on the same `(label, propertyName)` pair. - -## Write Flows - -There are two ways to push vectors into an external index. - -### Option A — Inline at write time - -Attach vectors directly inside any record create or import call. The index must already exist before vectors are written. - -```typescript -await db.records.create('Article', { - title: 'Understanding Graph RAG', - body: 'Graphs provide context that plain vector search lacks...', - __vectors: [ - { - propertyName: 'body', - vector: await embed('Understanding Graph RAG...') // your embedding function - } - ] -}) -``` - -This is the lowest-latency path: one round-trip creates the record and stores its vector. - -### Option B — Upsert after the fact - -Push vectors separately, useful for seeding an index from an existing dataset or syncing after a batch embedding job. - -```typescript -await db.ai.indexes.upsertVectors(indexId, { - items: [ - { recordId: 'rec_001', vector: [0.1, 0.2, ...] }, - { recordId: 'rec_002', vector: [0.7, 0.8, ...] } - ] -}) -``` - -The upsert call is **idempotent** — re-running it with the same `recordId` replaces the stored vector. - -## Searching with a Pre-computed Vector - -Once vectors are stored, search with `queryVector` instead of `query`: - -```typescript -const results = await db.ai.search({ - label: 'Article', - propertyName: 'body', - queryVector: await embed('graph databases and retrieval'), // your embedding function - limit: 10 -}) -``` - -The result shape is the same as a managed semantic search — records ranked by cosine (or euclidean) similarity, with an optional `__score` field. - -## Lifecycle - -```mermaid -graph LR - A["Create external index
sourceType: external
dimensions: N"] --> B["awaiting_vectors"] - B --> C["Write first vector
(inline or upsertVectors)"] - C --> D["ready"] - D --> E["Search with queryVector"] -``` - -An external index stays in `awaiting_vectors` until at least one vector has been written. After that it is `ready` and searchable. - ---- - -## Implementation Reference - -
- - TypeScript SDK - - db.ai.indexes · upsertVectors · ai.search - - - - Python SDK - - db.ai.indexes · upsert_vectors · ai.search - - - - REST API - - POST /ai/indexes · /vectors/upsert · BYOV guide - - -
- -
- - - Tutorial — BYOV External Embeddings - - - Step-by-step walkthrough with TypeScript, Python, and Shell examples - - -
diff --git a/docs/docs/concepts/data-ingestion.mdx b/docs/docs/concepts/data-ingestion.mdx deleted file mode 100644 index c6f3426b..00000000 --- a/docs/docs/concepts/data-ingestion.mdx +++ /dev/null @@ -1,145 +0,0 @@ ---- -sidebar_position: 8 ---- - -# Data Ingestion - -RushDB accepts raw data — JSON objects, nested trees, flat arrays, or CSV — and turns it into a fully typed, linked graph. No schema definitions, no migrations, no manual relationship wiring. - -## How It Works - -Every ingestion call goes through the same pipeline: - -```mermaid -graph LR - A["Input
JSON / CSV"] --> B["BFS
parse"] - B --> C["Type
inference"] - C --> D["Label
assignment"] - D --> E["Relationship
creation"] - E --> F["Graph
stored"] -``` - -1. **Parse** — RushDB walks the input with a breadth-first search (BFS) algorithm. Each nested object becomes a separate record. -2. **Type inference** — Every value is classified as `string`, `number`, `boolean`, `datetime`, or `null`. Arrays must contain a consistent type. Mixed types that can be coerced (e.g. `"42"` → `42`) are converted automatically; otherwise the property falls back to `string`. -3. **Label assignment** — Top-level records use the label you provide. Nested objects derive their label from the parent key name (e.g. a key `"engine"` produces label `Engine`). -4. **Relationship creation** — Parent → child records are linked with default relationships (`__RUSHDB__RELATION__DEFAULT__`). Property nodes are connected via value relationships. - -The result is a fully navigable graph — queryable by field values, labels, relationships, or vector similarity — all from a single push. - -## Input Formats - -### Nested JSON (`importJson`) - -Use `importJson` when your data contains nested objects, arrays of objects, or hash-map-like structures. RushDB decomposes the tree into linked records automatically. - -```json -{ - "company": { - "name": "Acme Corp", - "founded": "2020-01-15T00:00:00Z", - "departments": [ - { - "name": "Engineering", - "headcount": 42 - }, - { - "name": "Design", - "headcount": 12 - } - ] - } -} -``` - -This single call produces **4 records** (`Company`, `Departments` × 2) with default relationships linking them, plus property nodes for every field — all types inferred. - -### Flat arrays (`createMany`) - -Use `createMany` when your input is an array of flat, row-like objects (no nesting). This is the fastest path for CSV-shaped data. - -```json -[ - { "name": "Alice", "email": "alice@example.com", "age": 30 }, - { "name": "Bob", "email": "bob@example.com", "age": 25 } -] -``` - -### CSV strings (`importCsv`) - -Pass a raw CSV string and RushDB will parse, infer types, and import. Delimiter, quoting, and numeric conversion are configurable. - -```csv -name,email,age -Alice,alice@example.com,30 -Bob,bob@example.com,25 -``` - -## Upsert: Merge or Replace - -All three methods support upsert via `mergeBy` and `mergeStrategy`: - -| Option | Description | -|---|---| -| `mergeBy` | Array of property names used to match existing records (e.g. `["email"]`). If empty or omitted with `mergeStrategy` present, all incoming keys are used. | -| `mergeStrategy: 'append'` | (Default) Adds or updates incoming properties. Existing properties not mentioned in the payload are preserved. | -| `mergeStrategy: 'rewrite'` | Replaces all properties on the matched record with the incoming payload. Unmentioned properties are removed. | - -## Import Options - -| Option | Description | -|---|---| -| `suggestTypes` | When `true`, RushDB infers types from values instead of storing everything as `string`. Enabled by default in most SDK methods. | -| `capitalizeLabels` | Capitalise auto-derived labels (e.g. `departments` → `Departments`). | -| `returnResult` | Return the created/updated records in the response. | - -## How Nested Data Becomes a Graph - -Consider this payload: - -```json -{ - "car": { - "make": "Tesla", - "model": "Model 3", - "engine": { - "power": 283, - "type": "electric" - } - } -} -``` - -RushDB produces: - -```mermaid -graph TD - Car["Car
make: Tesla
model: Model 3"] -->|DEFAULT| Engine["Engine
power: 283
type: electric"] -``` - -- The outer key `"car"` becomes the label **Car**. -- The nested key `"engine"` becomes a separate record with label **Engine**. -- A default relationship links Car → Engine. -- All property types are inferred (`make: string`, `power: number`, etc.). - -See [Records](./records.md) for more on record structure, [Relationships](./relationships.mdx) for relationship types, and [Properties](./properties.md) for type inference details. - ---- - -## Implementation Reference - -Each interface covers the full ingestion API — pick the one that fits your stack: - -
- - TypeScript SDK - createMany · importJson · importCsv - - - Python SDK - create_many · import_csv - - - REST API - POST /records/import/json · /records/import/csv - -
diff --git a/docs/docs/concepts/index.mdx b/docs/docs/concepts/index.mdx deleted file mode 100644 index a9ff5373..00000000 --- a/docs/docs/concepts/index.mdx +++ /dev/null @@ -1,307 +0,0 @@ ---- -sidebar_position: 0 -title: Main Concepts -description: The fundamental ideas behind RushDB — how it stores, links, and queries your data. ---- - -import Tabs from '@site/src/components/LanguageTabs' -import TabItem from '@theme/TabItem' - -# Main Concepts - -RushDB is a **graph database built for developers and AI agents**. You push raw data — JSON objects, nested trees, flat arrays — and RushDB automatically infers types, decomposes nested structures into linked records, and builds a fully traversable graph. No schema definitions. No migration files. No manual relationship wiring. - -This page gives you the mental model you need to work with RushDB effectively. Each section links to a deeper reference when you're ready for it. - ---- - -## Records - -A **Record** is the fundamental unit of data in RushDB. Think of it as a typed row in a table, a document in a document store, or a node in a graph — but with seamless relationship traversal built in. - -Every record has: - -- A **label** — a category name like `User`, `Article`, or `Product` -- **Properties** — typed key-value fields (`name: "John"`, `score: 4.9`, `active: true`) -- A system-generated **ID** (UUIDv7) — lexicographically sortable, embeds a creation timestamp - -```typescript -{ - // ── generated on write ────────────────────────────────────── - "__id": "01968aa4-22c1-781a-8e8c-8fe6be6c3fd4", // UUIDv7; embeds creation timestamp - "__label": "User", // record type - "__proptypes": { // types inferred from your data - "name": "string", - "email": "string", - "rating": "number", - "emailConfirmed": "boolean", - "registeredAt": "datetime" - }, - - // ── written by you ────────────────────────────────────────── - "name": "John Galt", - "email": "john.galt@example.com", - "rating": 4.98, - "emailConfirmed": true, - "registeredAt": "2022-07-19T08:30:28.000Z" -} -``` - -Records never require a predefined schema. RushDB infers the type of every value at write time. - -→ [Records in depth](./records.md) - ---- - -## Labels - -A **Label** is the type name assigned to a record — `User`, `Car`, `Invoice`. Labels work like table names in a relational database but without the rigidity: there is no table to define ahead of time. - -Key characteristics: - -- Every record has exactly **one custom label** (required) -- Labels are **case-sensitive** (`User` ≠ `user`) -- When importing nested JSON, child objects automatically inherit their label from the **parent key name** — no manual assignment needed - -Labels are the primary lens for filtering: `labels: ["User", "Admin"]` in a search query. - -→ [Labels in depth](./labels.md) - ---- - -## Properties - -A **Property** is a named, typed field shared across all records that carry it. In RushDB's internal graph model, properties are first-class nodes — not just columns — which means the same `color` property node connects every `Car`, `Jacket`, and `House` record that has a color field. - -Property nodes hold **only the `(name, type)` pair** — no values. Actual values live exclusively on the Record node. This means Property nodes act as schema/metadata guardrails: they define what fields exist and what types they carry, while records remain the sole source of truth for user-defined data. - -This design enables a capability unique to RushDB: **discovering relationships between otherwise unconnected records** by finding records that share the same property value — without duplicating any data in the property layer. - -Supported types: `string`, `number`, `boolean`, `datetime`, `null`, and arrays of a consistent type. - -→ [Properties in depth](./properties.md) - ---- - -## Relationships - -**Relationships** are the edges that connect records in the graph. They are first-class citizens — not foreign key references, not join tables — which means traversing them is fast regardless of dataset size. - -RushDB manages two relationship types automatically: - -| Type | Created by | Purpose | -| --------------------------------------------- | --------------- | --------------------------------------- | -| **Default** (`__RUSHDB__RELATION__DEFAULT__`) | Data ingestion | Parent → child links for nested objects | -| **Value** (`__RUSHDB__RELATION__VALUE__`) | Property system | Property node → Record connections | - -You can also define **custom relationships** of any type and direction between any two records — created, updated, and deleted through the API or SDKs at any time. - -→ [Relationships in depth](./relationships.mdx) - ---- - -## Data Ingestion - -RushDB accepts data the way it arrives — no upfront schema required. The ingestion pipeline does five things automatically: - -1. **Parse** — Walk the input with a breadth-first algorithm. Each nested object becomes a separate record. -2. **Infer types** — Every value is classified as `string`, `number`, `boolean`, `datetime`, or `null`. -3. **Assign labels** — Top-level records use the label you provide; nested objects derive theirs from the parent key name. -4. **Wire relationships** — Parent and child records are linked with default relationships. -5. **Index properties** — Property nodes are created and connected to each record via value relationships. - -A single `importJson` call on this input: - -```json -{ - "company": { - "name": "Acme Corp", - "founded": "2020-01-15T00:00:00Z", - "departments": [ - { "name": "Engineering", "headcount": 42 }, - { "name": "Design", "headcount": 12 } - ] - } -} -``` - -produces **4 linked records** (`Company`, `Department` × 2) — all typed, all connected. - -For flat, row-shaped data (CSV-like), use `createMany` instead — it skips the BFS decomposition and is the fastest write path. - -→ [Data Ingestion in depth](./data-ingestion.mdx) - ---- - -## Search - -All queries in RushDB use a single, composable **SearchQuery** structure. The `where` clause supports exact match, range, text contains, negation, boolean logic, and **multi-hop graph traversal** — querying across relationships in the same expression: - - - - -```typescript -const { data } = await db.records.find({ - labels: ['Article'], - where: { - status: 'published', - Author: { - // traverse the relationship to Author records - country: 'Germany' - } - }, - orderBy: { registeredAt: 'desc' }, - limit: 20 -}) -``` - - - - -```python -results = db.records.find({ - "labels": ["Article"], - "where": { - "status": "published", - "Author": { # traverse the relationship to Author records - "country": "Germany" - } - }, - "orderBy": { "registeredAt": "desc" }, - "limit": 20 -}) -``` - - - - -```bash -curl -X POST "https://api.rushdb.com/api/v1/records/search" \ - -H "Authorization: Bearer $RUSHDB_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "labels": ["Article"], - "where": { - "status": "published", - "Author": { "country": "Germany" } - }, - "orderBy": { "registeredAt": "desc" }, - "limit": 20 - }' -``` - - - - -→ [Search in depth](./search/introduction.md) - ---- - -## Semantic Search - -RushDB lets you search by **meaning**, not just exact values. Create an embedding index on any string property and every record that carries that property becomes searchable by natural-language similarity — while still composing with all standard field filters. - - - - -```typescript -const { data } = await db.ai.search({ - propertyName: 'description', - query: 'space exploration', - labels: ['Movie'], - where: { genre: 'sci-fi', year: { $gte: 2000 } }, - limit: 10 -}) -``` - - - - -```python -results = db.ai.search({ - "propertyName": "description", - "query": "space exploration", - "labels": ["Movie"], - "where": { "genre": "sci-fi", "year": { "$gte": 2000 } }, - "limit": 10 -}) -``` - - - - -```bash -curl -X POST "https://api.rushdb.com/api/v1/ai/search" \ - -H "Authorization: Bearer $RUSHDB_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "propertyName": "description", - "query": "space exploration", - "labels": ["Movie"], - "where": { "genre": "sci-fi", "year": { "$gte": 2000 } }, - "limit": 10 - }' -``` - - - - -Two modes are available: - -- **Managed** — RushDB generates and stores embeddings automatically. -- **External (BYOV)** — Your application supplies pre-computed vectors; RushDB stores and indexes them. - -Both modes use Neo4j's native vector index and compose fully with the structured `where` clause. - -→ [Semantic Search in depth](./semantic-search.mdx) · [Bring Your Own Vectors](./bring-your-own-vectors.mdx) - ---- - -## Transactions - -A **Transaction** groups any number of read and write operations into a single atomic unit. Either all operations succeed and are committed, or none of them persist. - -RushDB transactions are built on Neo4j's native ACID guarantees: - -- **Atomicity** — All-or-nothing. No partial writes. -- **Consistency** — The database moves from one valid state to another. -- **Isolation** — Concurrent transactions do not interfere with each other. -- **Durability** — Committed changes survive system failures. - -Transactions have a configurable TTL. If the client does not commit before expiry, the transaction is automatically rolled back. - -→ [Transactions in depth](./transactions.mdx) - ---- - -## Storage Architecture - -RushDB uses a **dual storage model**: - -- **Neo4j** — stores all records, properties, and relationships. Graph traversal, vector similarity search, and ACID transactions all run here. -- **SQL** (SQLite locally, PostgreSQL in production) — stores operational metadata: users, workspaces, projects, and API tokens. - -This separation keeps account management in a familiar relational layer while keeping all knowledge graph operations in Neo4j, where they are most efficient. - -→ [Storage in depth](./storage.md) - ---- - -## RushDB as Agent Memory - -RushDB is designed to serve as **structured long-term memory for AI agents**. It provides three memory layers out of the box: - -| Layer | What it stores | RushDB primitive | -| -------------- | -------------------------------------------------------- | ------------------------------ | -| **Episodic** | Facts, events, entities, and their connections | Records + Relationships | -| **Semantic** | Meaning encoded as dense vectors | Vector Properties + AI Indexes | -| **Structural** | Schema: what labels, properties, and relationships exist | Ontology API | - -A typical agent retrieval flow: - -1. Call the **Ontology API** to discover what labels and properties exist in the current project. -2. Use a structured **`where` filter** to narrow candidates by known field values. -3. Apply a **`vector.similarity`** aggregation to re-rank by semantic relevance. -4. Return scored, fully structured records — not raw text chunks — to the agent. - -→ [Agent Memory Model in depth](./agent-memory-model.md) · [Ontology & Schema Discovery](./ontology-schema-discovery.md) diff --git a/docs/docs/concepts/labels.md b/docs/docs/concepts/labels.md deleted file mode 100644 index 7ba80478..00000000 --- a/docs/docs/concepts/labels.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -sidebar_position: 3 ---- -# Labels - -Labels in RushDB are an essential part of the database schema, providing a way to categorize and organize records. - -## How it works - -Every record in RushDB has two labels: -1. A default system label (`__RUSHDB__LABEL__RECORD__`). This label is never exposed publicly. -2. A user-defined label that is searchable (e.g., `User`, `Car`, `Product`) - -Labels help in organizing your data and enable efficient querying across similar types of records. They function similarly to table names in relational databases but with the flexibility of graph databases. - -```typescript -// Record with label "User" -{ - "__id": "01968aa4-22c1-781a-8e8c-8fe6be6c3fd4", - "__label": "User", // User-defined label (required and limited to one per record) - "__proptypes": { - "name": "string", - "emailConfirmed": "boolean", - "registeredAt": "datetime", - "rating": "number", - "currency": "string", - "email": "string" -}, - "name": "John Galt", - "emailConfirmed": true, - "registeredAt": "2022-07-19T08:30:28.000Z", - "rating": 4.98, - "currency": "USD", - "email": "john.galt@example.com" -} -``` - -## Label Requirements and Limitations - -Currently, RushDB has the following requirements for labels: - -1. **Single Custom Label**: Each record can have only one custom label at a time. -2. **Required Field**: A custom label is required for each record by default. -3. **Case-Sensitive**: Labels are case-sensitive, so "User" and "user" would be considered different labels. - -These requirements help maintain a clean and consistent data structure across your database. For more details on how labels interact with other database elements, see [Records](../concepts/records) and [Properties](../concepts/properties). - -## Label Assignment - -Labels can be: -1. **Explicitly provided** by the user for top-level records -2. **Automatically derived** from parent keys for nested objects during the data import process - -When importing nested JSON data, RushDB's breadth-first search algorithm automatically assigns appropriate labels based on the parent keys, making the process intuitive without requiring manual schema design. - -For example, when importing this JSON: - -```json -{ - "car": { - "make": "Tesla", - "model": "Model 3", - "engine": { - "power": 283, - "type": "electric" - } - } -} -``` - -The label "car" is assigned to the top record, and "engine" is assigned to the nested record. - -## Internal Representation - -Internally, labels are stored as the `__RUSHDB__KEY__LABEL__` property and exposed to clients as `__label`. This property is essential for organizing records and enabling efficient queries across similar types of data. - -To learn more about how records are structured and interconnected, see [Records](../concepts/records) and [Relationships](../concepts/relationships). - diff --git a/docs/docs/concepts/ontology-schema-discovery.md b/docs/docs/concepts/ontology-schema-discovery.md deleted file mode 100644 index 71f8dd71..00000000 --- a/docs/docs/concepts/ontology-schema-discovery.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Ontology & Schema Discovery - -The **Ontology API** returns a live, computed snapshot of what exists in your RushDB project: every label, every property per label (with its type and value distribution), and the full relationship map between labels. Agents use it to bootstrap schema awareness at the start of a session — no hardcoded schema, no external documentation required. - -## Why This Matters for Agents - -Traditional databases assume that the application knows the schema ahead of time. This works when the schema is static and human-authored. AI agents face a different reality: - -- The knowledge graph may have been populated by other agents, batch imports, or live event streams. -- The schema drifts over time as new label types and properties appear. -- Agents cannot be pre-programmed with field names they have never seen. - -RushDB's answer is **schema-on-read for agents**: call `/ai/ontology/md` at the start of each session and receive the full, current schema as a single response. The agent can then construct valid `SearchQuery` objects referencing only labels and properties that actually exist. - ---- - -## What the Ontology Contains - -| Component | Description | -|---|---| -| **Label inventory** | All label names currently in the project, with record counts | -| **Property manifest per label** | Property name, type, and either sample values (strings/booleans) or a min–max range (numbers/datetimes) | -| **Relationship map** | Which labels connect to which, via which relationship type, and in which direction | - -This is sufficient for an agent to: - -1. Know which labels exist (and how many records each has) -2. Know which fields are queryable on each label and what values they carry -3. Know which graph traversals are valid (which labels are reachable from which) -4. Construct faceted filter ranges without any extra round-trip queries - ---- - -## Two Formats - -### Markdown — for LLM context injection - -`POST /api/v1/ai/ontology/md` - -Returns the schema as compact Markdown tables. This format is optimised for direct injection into an LLM system prompt or tool result — token-efficient, human-readable, and immediately usable by a language model. - -```text -# Graph Ontology - -## Labels - -| Label | Count | -|-----------|------:| -| `Article` | 4821 | -| `Author` | 312 | -| `Tag` | 87 | - ---- - -## `Article` (4821 records) - -### Properties - -| Property | Type | Values / Range | -|---------------|----------|----------------------------------------------| -| `title` | string | `"Graph databases…"`, `"Intro to…"` (+4819) | -| `published` | boolean | `true`, `false` | -| `score` | number | `0.0`..`9.8` | -| `publishedAt` | datetime | `2020-01-01`..`2026-03-29` | - -### Relationships - -| Type | Direction | Other Label | -|---------------|-----------|-------------| -| `WRITTEN_BY` | out | `Author` | -| `TAGGED_WITH` | out | `Tag` | -``` - -### JSON — for programmatic tool calls - -`POST /api/v1/ai/ontology` - -Returns the same data as a structured JSON array. Use this format when an agent needs to programmatically extract specific labels or property names before constructing a query. - ---- - -## The Self-Awareness Loop - -```mermaid -sequenceDiagram - participant Agent - participant RushDB - - Agent->>RushDB: POST /ai/ontology/md - RushDB-->>Agent: Markdown schema (labels, properties, relationships, values) - Note over Agent: Agent knows what exists in the graph - - Agent->>RushDB: records.find({ labels: ['Article'], where: { published: true } }) - RushDB-->>Agent: Matching records - - Note over Agent: Agent constructs response from real, structured data -``` - -The loop is stateless from RushDB's side. The agent calls the ontology endpoint whenever it needs a fresh view of the schema, then proceeds to query. There is no session to open or schema to register. - ---- - -## Dynamic Facet Discovery - -Because the ontology includes value distributions for each property, agents can construct UI filters or reasoning steps entirely from the ontology response — no separate "what values exist?" queries needed. - -**Example:** An agent building a product-search interface calls `/ai/ontology/md`, sees that the `Product` label has a `status` string property with sample values `["available", "discontinued", "pre-order"]`, and renders those as filter chips — without ever querying records for distinct values. - -**Example:** An agent reasoning over a CRM dataset calls `/ai/ontology`, sees that `Deal.amount` ranges from `500` to `250000`, and uses that range to construct a meaningful `$gte` / `$lte` filter in the next query. - ---- - -## Caching - -The ontology is computed once and cached by RushDB. Cache entries are invalidated when labels, properties, or relationships change in the project. Calling `/ai/ontology/md` at the start of every agent session incurs negligible overhead — a single network round trip returning a compact text payload. - ---- - -## Further Reading - -- [Labels](./labels.md) — how labels work as the primary organisational axis -- [Properties](./properties.md) — property types and the `__proptypes` self-description mechanism -- [Agent Memory Model](./agent-memory-model.md) — where ontology fits in the full retrieval stack -- [AI & Semantic Search](../../rest-api/ai/overview) — complete REST API reference for ontology endpoints diff --git a/docs/docs/concepts/properties.md b/docs/docs/concepts/properties.md deleted file mode 100644 index 5eec28a0..00000000 --- a/docs/docs/concepts/properties.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Properties - -The fundamental unit of meaningful data in RushDB is known as a **Property**. Properties are first-class citizens in the RushDB architecture and serve as critical links that interconnect diverse data within [Records](../concepts/records) across the graph database. - -## How it works - -In RushDB's unique property graph model, properties connect records through a combination of field name and data type. This creates a powerful network that reveals relationships that might otherwise remain hidden. - -Here is a simplified diagram illustrating how properties appear in a graph: - -```mermaid -graph LR - P0((Property:color)) --> b[Matte black] --> R0((Record:Jacket)) - - P0 --> d[Pale green] --> R2((Record:House)) - P1 --> j[Villa Vista] --> R2 - P1((Property:name)) --> e[Porsche 911] --> R1 - P0 --> c[Dark grey] --> R1((Record:SportCar)) - P2((Property:maxSpeed)) --> k[295] --> R1 -``` - -And this is how those **Records** can be represented in code: - -```typescript -// Record:Jacket -const jacket = { - color: "Matte black", // Property `color` [string] -}; - -// Record:SportCar -const sportCar = { - name: "Porsche 911", // Property `name` [string] - color: "Dark grey", // Property `color` [string] - maxSpeed: 295, // Property `maxSpeed` [number] -}; - -// Record:House -const house = { - name: "Villa Vista", // Property `name` [string] - color: "Pale green", // Property `color` [string] -}; -``` - -## Internal Structure - -Internally, properties are stored as nodes with the label `__RUSHDB__LABEL__PROPERTY__` and contain only the name and type fields (not the actual values). The values are stored in the record nodes, and properties are connected to their records via `__RUSHDB__RELATION__VALUE__` relationships. - -```mermaid -graph LR - subgraph Properties - B[":__RUSHDB__LABEL__PROPERTY__
name: color
type: string"] - end - subgraph Records - A[":__RUSHDB__LABEL__RECORD__ :Car"] - C[":__RUSHDB__LABEL__RECORD__ :Flower"] - end - B -->|__RUSHDB__RELATION__VALUE__| A - B -->|__RUSHDB__RELATION__VALUE__| C -``` - -This approach enables: - -1. Performant queries across different record types -2. Discovery of hidden insights in data through property-based connections -3. Optimized graph traversals leveraging Neo4j's native capabilities - -## Considerations - -Real-world data can be considerably more intricate and may encompass all conceivable -[data types](../concepts/storage#data-types) within a single **Record**. -However, rest assured that RushDB adeptly manages this complexity without hesitation. Nevertheless, there are a few -important considerations you should be aware of: - -1. **Property** is designed to accommodate only consistent values. This means that RushDB will strive to retain the - original value type. However, if there are any inconsistent or non-convertible values in the data, RushDB will - automatically convert them to a _string_ type. - -Payload contains inconsistent values but can be converted to desired _number_ type: - -```js -{ - name: "Combination", - type: "number", - value: [4, 8, 15, 16, "23", "42.0"] -} -// ---> converts to -{ - name: "Combination", - type: "number", - value: [4, 8, 15, 16, 23, 42.0] -} -``` - -Payload contains inconsistent values but cannot be converted to desired _number_ type: - -```js -{ - name: "Secret", - type:"number", - value: [1, 2, 3, "jelly bear"] -} -// ---> converts to -{ - name: "Secret", - type: "string", - value: ["1", "2", "3", "jelly bear"] -} -``` - ---- - -2. When two (or more) properties with the same name but different types come into play, RushDB will maintain both Properties as separate entities. For instance: - -```js -[ - { - type: "Raincoat", - size: "M", - }, - { - type: "Cardigan", - size: 38, - }, -]; -``` - -Will be saved as distinct properties (size:string and size:number) connecting to their respective records. - -## Supported Data Types - -RushDB supports a variety of data types to accommodate diverse data needs in your applications: - -### String - -Used for any textual information with virtually unlimited length. - -```js -{ - name: "productName", - type: "string", - value: "Premium Leather Jacket" -} -``` - -### Number - -Accommodates both floating-point numbers and integers. - -```js -{ - name: "price", - type: "number", - value: 129.99 -} -``` - -### Boolean - -Represents true or false values. - -```js -{ - name: "inStock", - type: "boolean", - value: true -} -``` - -### Datetime - -Follows ISO 8601 format, including timezone information. - -```js -{ - name: "manufacturedAt", - type: "datetime", - value: "2025-03-15T14:30:00Z" -} -``` - -### Null - -Represents the absence of a value. - -```js -{ - name: "discount", - type: "null", - value: null -} -``` - -### Arrays - -RushDB also supports arrays as property values, but they must contain consistent value types: - -> **Note:** Every data type mentioned above supports an array representation. - -```js -// String array -{ - name: "categories", - type: "string", - value: ["outerwear", "winter", "premium"] -} - -// Number array -{ - name: "availableSizes", - type: "number", - value: [36, 38, 40, 42, 44] -} - -// Boolean array -{ - name: "features", - type: "boolean", - value: [true, false, true, true] -} -``` - -RushDB automatically handles type inference during data import, ensuring optimal storage and retrieval of your property values. If there are mixed types within an array that can be consistently converted (like strings to numbers), RushDB will attempt the conversion. However, if conversion isn't possible, it will default to the most accommodating type (usually string). - -## Multi-tenant Isolation - -Properties are not shared amongst projects (database instances), ensuring complete isolation in multi-tenant environments. Each project has its own set of property nodes, maintaining data security and isolation. - -For more information on how properties are imported and processed, see [REST API - Import Data](../rest-api/records/import-data). diff --git a/docs/docs/concepts/records.md b/docs/docs/concepts/records.md deleted file mode 100644 index e79f757d..00000000 --- a/docs/docs/concepts/records.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -sidebar_position: 1 ---- -# Records - -In RushDB, Records are fundamental data structures that store meaningful key-value data. Each Record consists of individual properties (key-value pairs) and can be connected to other Records through relationships. - -## How it works - -Records in RushDB are the fundamental units of structured knowledge. While the underlying engine uses a rich graph model to represent and connect data, from a developer perspective a Record is simply a typed key-value object containing properties — like a row in a database or a document in a document store, but with seamless relationship traversal built in. - -Each record in RushDB consists of: - -- User-defined properties (key-value pairs) -- System properties (prefixed with `__`) - -Below is an example of a record with the label "User": - -```typescript -{ - "__id": "01968aa4-22c1-781a-8e8c-8fe6be6c3fd4", // Unique identifier - "__label": "User", // User-defined label (required and limited to one per record) - "__proptypes": { // Property types - "name": "string", - "emailConfirmed": "boolean", - "registeredAt": "datetime", - "rating": "number", - "currency": "string", - "email": "string" - }, - "name": "John Galt", - "emailConfirmed": true, - "registeredAt": "2022-07-19T08:30:28.000Z", - "rating": 4.98, - "currency": "USD", - "email": "john.galt@example.com" -} -``` - -Or this example with the label "Coffee": -```typescript -{ - "__id": "01968aa4-88c1-781a-8e8c-8fc6be7c3fd4", - "__label": "Coffee", - "__proptypes": { - "origin": "string", - "process": "string", - "cupping": "number", - "inStock": "boolean", - "roasted": "datetime", - "notes": "string" - }, - "origin": "Guatemala", - "process": "washed", - "cupping": 86, - "inStock": true, - "roasted": "2023-07-20T14:50:00Z", - "notes": ["Nuts", "Caramel", "Lime"] -} -``` - -## Internal Structure - -Internally, each Record in RushDB is represented as a node with two labels: -1. The system label `__RUSHDB__LABEL__RECORD__` -2. A user-defined label (exposed as `__label`) - -In addition to user-defined properties, each Record contains several internal properties that enable advanced functionality: - -| Internal Key | Client Representation | Description | -|-------------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `__RUSHDB__KEY__ID__` | `__id` | UUIDv7 that enables lexicographic ordering without relying on user-defined fields like `createdAt`. RushDB SDKs support converting `__id` to timestamp and ISO8601 date. For more details, see [UUIDv7 specification](https://www.ietf.org/archive/id/draft-peabody-uuid-v7-01.html). | -| `__RUSHDB__KEY__PROPERTIES__META__` | `__proptypes` | Stringified meta-object holding the types of data in the current record, e.g., `{ name: "string", active: "boolean", ... }` | -| `__RUSHDB__KEY__LABEL__` | `__label` | Record Label identifier. Every record has two labels: a default one (`__RUSHDB__LABEL__RECORD__`) and a user-defined one that is searchable. Currently, RushDB allows only one custom label per record, and it is required by default. For more details about labels, see [Labels](../concepts/labels). | -| `__RUSHDB__KEY__PROJECT__ID__` | (not exposed) | Project identifier for multitenancy isolation. This property is never exposed to clients via UI or API. | - -## Supported Data Types - -RushDB supports a wide range of data types to accommodate diverse data needs: - -| Data Type | Description | Example | -|------------|----------------------------------------------|----------------------------| -| `string` | Textual information of unlimited length | `"Hello World"` | -| `number` | Both floating-point numbers and integers | `-120.209817`, `42` | -| `datetime` | ISO 8601 format, including timezones | `"2012-12-21T18:29:37Z"` | -| `boolean` | True or false values | `true`, `false` | -| `null` | Explicit null value | `null` | - -### Arrays - -RushDB supports arrays as property values, but they must contain consistent value types: - -Examples of valid arrays include: -- `["apple", "banana", "carrot"]` - string array -- `[4, 8, 15, 16, 23, 42]` - number array -- `["2023-09-17T02:47:54+04:00", "1990-08-18T04:35:00+05:00"]` - datetime array -- `[true, false, true, false, true]` - boolean array - -When records are imported, data types are automatically inferred and stored in the `__proptypes` metadata, which helps maintain type consistency across your database. - -## Creating Records - -Records can be created through: - -1. Direct creation via the API or SDK -2. Automatic creation during data import - -During record creation: -- A unique `__id` is automatically generated if not provided -- Property types are inferred from values when not specified -- Relationships are established based on data structure - -Learn more at [REST API - Create Records](../rest-api/records/create-records.md) or through the language-specific SDKs: -- [TypeScript SDK](../typescript-sdk/records/create-records.md) -- [Python SDK](../python-sdk/records/create-records.md) - -## Graph Representation - -In RushDB's underlying Neo4j database, records are represented as nodes in a property graph model: - -```mermaid -graph TD - subgraph Records - A[":__RUSHDB__LABEL__RECORD__ :Car"] - B[":__RUSHDB__LABEL__RECORD__ :Engine"] - end - subgraph Properties - C[":__RUSHDB__LABEL__PROPERTY__
name: make
type: string"] - D[":__RUSHDB__LABEL__PROPERTY__
name: model
type: string"] - E[":__RUSHDB__LABEL__PROPERTY__
name: power
type: number"] - end - - A -->|"__RUSHDB__RELATION__DEFAULT__"| B - C -->|"__RUSHDB__RELATION__VALUE__"| A - D -->|"__RUSHDB__RELATION__VALUE__"| A - E -->|"__RUSHDB__RELATION__VALUE__"| B -``` - -In this structure: -- Records are nodes with the label `__RUSHDB__LABEL__RECORD__` plus a user-defined label -- Records store actual property values directly as node attributes -- Properties are connected to records via `__RUSHDB__RELATION__VALUE__` relationships -- Related records are connected through `__RUSHDB__RELATION__DEFAULT__` relationships - -## Complex Data Structure - -RushDB's architecture allows for nested data structures where Records can contain other Records. When importing hierarchical data like JSON objects, RushDB automatically: - -1. Assigns unique IDs to all records -2. Applies appropriate labels based on parent keys or user specifications -3. Creates relationships between parent and child records -4. Stores property metadata for type inference - -This process enables you to structure your data in a natural, intuitive way while maintaining the graph-based relationships that power efficient queries and traversals. - -RushDB automatically manages parent-child relationships between records. When importing nested JSON: - -```json -{ - "user": { - "name": "Jane Doe", - "address": { - "city": "San Francisco", - "country": "USA" - } - } -} -``` - -RushDB creates: -1. A "user" record containing the name property -2. An "address" record containing city and country properties -3. A relationship from "user" to "address" of type `__RUSHDB__RELATION__DEFAULT__` - -Each nested object becomes its own record with: -- A label derived from its key in the parent object -- A unique ID (following UUIDv7 format) -- Default relationships connecting it to its parent - -This transforms into the following graph structure: - -```mermaid -graph TD - subgraph Records - A[":__RUSHDB__LABEL__RECORD__ :user
name: Jane Doe"] - B[":__RUSHDB__LABEL__RECORD__ :address
city: San Francisco
country: USA"] - end - subgraph Properties - C[":__RUSHDB__LABEL__PROPERTY__
name: name
type: string"] - D[":__RUSHDB__LABEL__PROPERTY__
name: city
type: string"] - E[":__RUSHDB__LABEL__PROPERTY__
name: country
type: string"] - end - - A -->|"__RUSHDB__RELATION__DEFAULT__"| B - C -->|"__RUSHDB__RELATION__VALUE__"| A - D -->|"__RUSHDB__RELATION__VALUE__"| B - E -->|"__RUSHDB__RELATION__VALUE__"| B -``` - -For details on how to import complex data structures, see [REST API - Import Data](../rest-api/records/import-data) or through the language-specific SDKs: -- [TypeScript SDK](../typescript-sdk/records/import-data) -- [Python SDK](../python-sdk/records/import-data) - -For details on how properties are stored and managed, see the [Properties](../concepts/properties) section. -For information about the underlying storage structure, visit the [Storage](../concepts/storage) section. diff --git a/docs/docs/concepts/relationships.mdx b/docs/docs/concepts/relationships.mdx deleted file mode 100644 index 93db5dc3..00000000 --- a/docs/docs/concepts/relationships.mdx +++ /dev/null @@ -1,261 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Relationships - -In RushDB, relationships are the connections that link Records together, creating a powerful graph structure that represents both the data itself and how different pieces of data relate to one another. These connections enable intuitive data modeling that aligns with how we naturally think about information and its associations. - -## Types of Relationships - -RushDB implements three main types of relationships: - -### 1. Default Relationships (`__RUSHDB__RELATION__DEFAULT__`) - -Default relationships connect related records, typically representing parent-child relationships in nested data structures. For example, a Car record might connect to an Engine record via a default relationship. - -```mermaid -graph TD - A[":__RUSHDB__LABEL__RECORD__ :Car"] -->|"__RUSHDB__RELATION__DEFAULT__"| B[":__RUSHDB__LABEL__RECORD__ :Engine"] - C[":__RUSHDB__LABEL__RECORD__ :Motorcycle"] -->|"__RUSHDB__RELATION__DEFAULT__"| D[":__RUSHDB__LABEL__RECORD__ :Engine"] -``` - -These relationships are automatically created during the data import process when nested objects are detected. Learn more at [REST API - Import Data](../rest-api/records/import-data) or through the language-specific SDKs: - -- [TypeScript SDK](../typescript-sdk/records/import-data) -- [Python SDK](../python-sdk/records/import-data) - -### 2. Value Relationships (`__RUSHDB__RELATION__VALUE__`) - -Value relationships connect Property nodes to their Record nodes. These relationships flow from Properties to Records, indicating which records have which properties. - -```mermaid -graph LR - C[":__RUSHDB__LABEL__PROPERTY__
name: color"] -->|"__RUSHDB__RELATION__VALUE__"| A[":__RUSHDB__LABEL__RECORD__ :Car"] - C -->|"__RUSHDB__RELATION__VALUE__"| F[":__RUSHDB__LABEL__RECORD__ :Flower"] -``` - -This structure allows for finding connections between otherwise unrelated records based on shared properties. - -> **Note:** RushDB manages Property-Record relationships (Value relationships) autonomously and doesn't provide APIs to manually interact with or modify this type of relationship. This design ensures data integrity and consistency within the graph model. - -### 3. Custom Relationships - -Beyond the built-in relationships that RushDB creates automatically during data import, users can define and reconstruct relationships manually in any direction and of any type needed. This flexibility enables sophisticated data modeling that precisely captures your domain's relationship semantics. - -You can create, modify, and delete relationships programmatically using the [REST API](../rest-api/relationships) or through the language-specific SDKs: - -- [TypeScript SDK](../typescript-sdk/relationships) -- [Python SDK](../python-sdk/relationships) - -This capability allows you to: - -- Define domain-specific relationship types (e.g., "BELONGS_TO", "MANAGES", "DEPENDS_ON") -- Create relationships between previously unconnected records -- Build complex graph structures that evolve over time -- Restructure relationships as your data model changes - -### Bulk creation and many-to-many caution - -RushDB supports a bulk relationship creation endpoint (`POST /relationships/create-many`) that can either: - -- join source and target records by equality on provided keys (the common case), or -- when explicitly requested, create a many-to-many (cartesian) set of relationships between all matched sources and targets. - -The many-to-many mode is opt-in and guarded: the request must set a flag (e.g. `manyToMany`) and provide non-empty `where` filters for both sides; otherwise the server requires keys to perform a safe equality join. This prevents accidental, unbounded cartesian products which can be expensive to execute and store. - -## Suggested Relationships in the Dashboard - -Imported data is often structurally useful but semantically incomplete. - -For nested JSON, RushDB can already see the parent-child structure and creates default relationships automatically. For flat data imported from systems like MongoDB, PostgreSQL exports, CSV, or external APIs, related records may arrive as separate collections with reference fields such as `userId`, `orderId`, or `addressRef`. In both cases, the ontology can describe the labels, properties, and existing edges, but it may not know the domain-specific relationship names you want to keep long term. - -The **Relationships** tab in the Dashboard helps close that gap. RushDB analyzes the project ontology after writes, suggests relationship patterns, and lets you approve only the patterns that match your model. - -> Suggested relationships require LLM analysis to be configured for the project. Without it, RushDB still creates default relationships for nested imports and supports manual relationship creation through the API and SDKs. - -```mermaid -sequenceDiagram - participant App as App / Import - participant RushDB - participant Ontology as Ontology API - participant LLM as Relationship Analyzer - participant UI as Dashboard - - App->>RushDB: Write or import records - RushDB-->>App: Write response returned - RushDB->>Ontology: Refresh schema snapshot - RushDB->>LLM: Analyze labels, fields, relationships - LLM-->>RushDB: Suggested patterns - UI->>RushDB: User approves a pattern - RushDB->>RushDB: Create or retype matching relationships - RushDB->>RushDB: Apply approved pattern to future writes -``` - -### Two kinds of suggestions - -RushDB distinguishes between two common cases. - -| Suggestion kind | When it appears | What approval does | -| ------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| **Match fields** | Two labels have reference-like fields, such as `ORDER.userId` and `USER.userId` | Creates relationships between matching records now and applies the same pattern to future writes | -| **Rename existing** | Nested JSON already created default relationships, but the edge type is generic | Replaces matching default relationships with a semantic type, such as `DEPARTMENT` -> `HAS_PROJECT` -> `PROJECT` | - -### Example: flat imported collections - -If users and orders are imported separately, no graph edge exists yet: - -```json -{ - "USER": [ - { "userId": "usr_001", "name": "Ava Chen" }, - { "userId": "usr_002", "name": "Noah Smith" } - ], - "ORDER": [ - { "orderId": "ord_101", "userId": "usr_001", "total": 129.5 }, - { "orderId": "ord_102", "userId": "usr_002", "total": 48.0 } - ] -} -``` - -The analyzer can suggest a **Match fields** pattern: - -```mermaid -graph LR - U["USER
userId = usr_001"] -->|"PLACED_ORDER"| O["ORDER
userId = usr_001"] -``` - -Approving the pattern creates matching relationships for existing records and keeps applying the same rule as more orders arrive. - -### Example: nested import with generic edges - -Nested payloads already contain structure: - -```json -{ - "DEPARTMENT": [ - { - "name": "Engineering", - "PROJECT": [{ "name": "Search relevance" }, { "name": "Ontology explorer" }] - } - ] -} -``` - -RushDB imports this as `DEPARTMENT` connected to `PROJECT` through default relationships. The analyzer should not invent a field match like `DEPARTMENT.name` -> `PROJECT.name`; those fields are descriptive, not references. Instead, it can suggest a **Rename existing** pattern: - -```mermaid -graph LR - BeforeA["DEPARTMENT"] -->|"__RUSHDB__RELATION__DEFAULT__"| BeforeB["PROJECT"] - AfterA["DEPARTMENT"] -->|"HAS_PROJECT"| AfterB["PROJECT"] -``` - -Approving this kind of pattern retypes the existing default relationships into semantic relationships. Future nested imports with the same structure can be upgraded the same way. - -### Why this helps - -- **Less manual stitching:** You do not need to write one-off relationship scripts for every imported collection. -- **Safer graph evolution:** Suggestions stay in draft form until approved, and ignored suggestions can be removed later if you want them reconsidered. -- **Better ontology for agents:** Semantic relationship types make schema discovery more useful. An agent can reason over `USER -> PLACED_ORDER -> ORDER` more reliably than over a generic default edge. -- **Lower write latency:** Relationship discovery and application run as side effects after writes, so record writes can return without waiting for graph enrichment to finish. - -## Nested Data Example - -Consider this JSON structure: - -```json -{ - "Person": { - "Name": "John Galt", - "Age": 30, - "Contact": { - "Email": "john.galt@example.com", - "Phone": "123-456-7890" - }, - "Address": { - "Street": "123 Main Street", - "City": "Anytown", - "State": "CA", - "ZipCode": "12345" - } - } -} -``` - -When imported into RushDB, this is transformed into a graph structure with: - -- 3 Records (Person, Contact, and Address) -- 8 Properties (Name, Age, Email, Phone, Street, City, State, ZipCode) -- Default relationships connecting Person to Contact and Person to Address - -```mermaid -graph LR - Person[":__RUSHDB__LABEL__RECORD__ :Person"] -->|"__RUSHDB__RELATION__DEFAULT__"| Contact[":__RUSHDB__LABEL__RECORD__ :Contact"] - Person -->|"__RUSHDB__RELATION__DEFAULT__"| Address[":__RUSHDB__LABEL__RECORD__ :Address"] - - Name[":__RUSHDB__LABEL__PROPERTY__
name: Name"] -->|"__RUSHDB__RELATION__VALUE__"| Person - Age[":__RUSHDB__LABEL__PROPERTY__
name: Age"] -->|"__RUSHDB__RELATION__VALUE__"| Person - - Email[":__RUSHDB__LABEL__PROPERTY__
name: Email"] -->|"__RUSHDB__RELATION__VALUE__"| Contact - Phone[":__RUSHDB__LABEL__PROPERTY__
name: Phone"] -->|"__RUSHDB__RELATION__VALUE__"| Contact - - Street[":__RUSHDB__LABEL__PROPERTY__
name: Street"] -->|"__RUSHDB__RELATION__VALUE__"| Address - City[":__RUSHDB__LABEL__PROPERTY__
name: City"] -->|"__RUSHDB__RELATION__VALUE__"| Address - State[":__RUSHDB__LABEL__PROPERTY__
name: State"] -->|"__RUSHDB__RELATION__VALUE__"| Address - ZipCode[":__RUSHDB__LABEL__PROPERTY__
name: ZipCode"] -->|"__RUSHDB__RELATION__VALUE__"| Address -``` - -## Data Import Process - -RushDB's data import mechanism uses a breadth-first search (BFS) algorithm to parse JSON structures and establish relationships: - -1. Nested objects are detected and converted to separate Record nodes -2. Parent-child relationships are established via `__RUSHDB__RELATION__DEFAULT__` edges -3. Property nodes are connected to their respective Record nodes via `__RUSHDB__RELATION__VALUE__` edges - -This approach allows for intuitive transformation of hierarchical data into a graph structure without requiring users to understand the underlying graph model. - -## Benefits of RushDB's Relationship Structure - -This relationship model provides several advantages: - -1. **Intuitive Data Modeling**: You can structure your data in a way that matches how you think about it -2. **Efficient Traversals**: The graph structure enables fast navigation between related records -3. **Hidden Insights**: Property connections can reveal relationships between seemingly unrelated records -4. **Flexible Structure**: Relationships can be easily rearranged or modified as your data model evolves - ---- - -## Implementation Reference - -Each interface covers creating, querying, and managing relationships — pick the one that fits your stack: - -
- - TypeScript SDK - - attach · detach · relationships API - - - - Python SDK - - attach · detach · relationships API - - - - REST API - - POST /relationships · bulk create - - -
diff --git a/docs/docs/concepts/search/_category_.json b/docs/docs/concepts/search/_category_.json deleted file mode 100644 index d838fa97..00000000 --- a/docs/docs/concepts/search/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Search", - "position": 2, - "collapsible": true, - "collapsed": true -} diff --git a/docs/docs/concepts/semantic-search.mdx b/docs/docs/concepts/semantic-search.mdx deleted file mode 100644 index 2155d545..00000000 --- a/docs/docs/concepts/semantic-search.mdx +++ /dev/null @@ -1,171 +0,0 @@ ---- -sidebar_position: 9 ---- - -import Tabs from '@site/src/components/LanguageTabs' -import TabItem from '@theme/TabItem' - -# Semantic Search - -RushDB lets you search records by **meaning**, not just exact field values. Create an embedding index on any string property and every record that carries that property becomes searchable by natural-language similarity — while still supporting all the usual field filters, pagination, and graph traversal. - -## How It Works - -```mermaid -graph LR - A["String property
e.g. description"] --> B["Embedding
index"] - B --> C["Vector stored
on relationship"] - D["Search query
text or vector"] --> E["Similarity
ranking"] - C --> E - E --> F["Scored
results"] -``` - -1. **Create an index** — Pick a label and a string property. RushDB creates a vector index policy. -2. **Embed** — For **managed** indexes, RushDB generates embeddings automatically on write and backfills existing records. For **external** (BYOV) indexes, your application supplies pre-computed vectors. -3. **Search** — Pass a natural-language query (managed) or a pre-computed vector (external). RushDB ranks candidates by cosine or euclidean similarity and returns scored results. - -## Managed vs. External Indexes - -| Aspect | Managed | External (BYOV) | -| ----------------------- | --------------------------------- | ----------------------------------------------------- | -| Embeddings generated by | RushDB (server-side) | Your application | -| Write flow | Automatic on record create/update | Supply vectors via `upsertVectors` or inline on write | -| Search input | Natural-language `query` string | Pre-computed `queryVector` array | -| Model control | RushDB-managed model | Any model, any dimension | - -Both types store vectors on the value relationship between the property node and the record node, using Neo4j's native vector index for fast retrieval. - -## Why Vectors Live on Property–Record Edges - -RushDB stores embeddings **on the edge** between the Property node and the Record node, not on the record itself. This is a deliberate design choice with several concrete benefits: - -- **One index spans every label.** A Property node (e.g. `description:string`) is shared across all records that carry that field, regardless of their label. A vector index on the Property→Record edge therefore covers `Article`, `Product`, `Movie`, and any other label with a `description` field — from a single index, with no duplication. -- **Records stay clean.** Record nodes contain only typed scalar properties. Embedding arrays — which can be hundreds or thousands of floats — live on the relationship layer. This keeps record storage lean and read performance high for non-vector queries. -- **Graph traversal and similarity compose naturally.** Because the vector is a property of a graph edge, Neo4j can traverse relationships and apply cosine scoring in the same query step. Multi-hop traversal followed by semantic re-ranking requires no extra join or post-processing pass. -- **Automatic candidate scoping.** Only records actually connected to the indexed Property node are ever considered for similarity ranking. Tenant isolation, label filtering, and `where` prefiltering all reduce the candidate set through normal graph traversal — no metadata tricks required. - -## Combining with Field Filters - -Semantic search is not an either/or — it composes with RushDB's structured query capabilities. Pass a `where` clause to pre-filter candidates before similarity ranking: - - - - -```typescript -const { data } = await db.ai.search({ - propertyName: 'description', - query: 'space exploration', - labels: ['Movie'], - where: { genre: 'sci-fi', year: { $gte: 2000 } }, - limit: 10 -}) -// results ranked by cosine similarity, scoped to sci-fi films from 2000+ -``` - - - - -```python -results = db.ai.search({ - "propertyName": "description", - "query": "space exploration", - "labels": ["Movie"], - "where": { "genre": "sci-fi", "year": { "$gte": 2000 } }, - "limit": 10 -}) -# results.data ranked by cosine similarity, scoped to sci-fi films from 2000+ -``` - - - - -```bash -curl -X POST "https://api.rushdb.com/api/v1/ai/search" \ - -H "Authorization: Bearer $RUSHDB_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "propertyName": "description", - "query": "space exploration", - "labels": ["Movie"], - "where": { "genre": "sci-fi", "year": { "$gte": 2000 } }, - "limit": 10 - }' -``` - - - - -This narrows the vector search to only matching records, keeping results precise and fast. - -## Two Ways to Search Semantically - -### 1. `db.ai.search()` — dedicated semantic endpoint - -The simplest path. Returns records ranked by similarity score (`__score`): - -- Accepts `query` (text) for managed indexes or `queryVector` for external indexes. -- Supports `where` pre-filtering, `limit`, and `skip`. -- Results always ordered by `__score` descending (best match first). - -### 2. `vector.similarity` aggregation in SearchQuery - -For advanced use cases, add a `vector.similarity.cosine` or `vector.similarity.euclidean` aggregation to any `db.records.find()` call. This gives you the full SearchQuery feature set (groupBy, collect, multi-hop relationships) alongside similarity scoring. - -→ See [Search — Select Expressions](./search/select.md) for the aggregation syntax. - -## Index Lifecycle - -| State | Description | -| ---------- | --------------------------------------------------------------------- | -| `pending` | Index created, backfill not yet started. | -| `indexing` | Backfill in progress — existing records are being embedded. | -| `ready` | All records indexed. New records are embedded on write automatically. | - -You can check index status at any time and list all indexes for a project. - -## When to Use Semantic Search - -| Scenario | Approach | -| -------------------------------------------------------- | -------------------------------------------------------- | -| User knows the exact value | Structured `where` filter | -| User describes what they want in natural language | `db.ai.search()` with `query` | -| Combine meaning + exact constraints | `db.ai.search()` with `where` pre-filter | -| Need groupBy, collect, or multi-hop alongside similarity | `db.records.find()` with `vector.similarity` aggregation | - -→ See also [Agent Memory Model](./agent-memory-model.md) for how semantic search fits into the three-layer retrieval stack. - ---- - -## Implementation Reference - -Each interface covers search, indexing, and BYOV — pick the one that fits your stack: - -
- - TypeScript SDK - - db.ai.search · db.ai.indexes · BYOV - - - - Python SDK - - db.ai.search · db.ai.indexes · BYOV - - - - REST API - - POST /ai/search · /ai/indexes · BYOV - - -
diff --git a/docs/docs/concepts/storage.md b/docs/docs/concepts/storage.md deleted file mode 100644 index e0eac236..00000000 --- a/docs/docs/concepts/storage.md +++ /dev/null @@ -1,326 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Storage - -RushDB uses a **dual storage architecture**: - -- **[Neo4j](https://neo4j.com/docs/get-started/get-started-with-neo4j/) (version 2026.01.4 or higher)** — stores all user-defined records and properties. The [APOC](https://neo4j.com/labs/apoc/) (Awesome Procedures On Cypher) plugin is required for JSON serialization, map/collection utilities, and property management. Vector similarity search uses Neo4j's **native vector index** (`db.index.vector.*` and `vector.similarity.cosine()`), available built-in from Neo4j 5.x / 2026.x — no additional plugin required. -- **SQL database (SQLite by default, PostgreSQL recommended for production)** — stores all dashboard entities: users, workspaces, projects, and API tokens. This layer is managed with [Drizzle ORM](https://orm.drizzle.team/) and runs migrations automatically on startup. - -## Graph Database vs. Traditional Databases - -Unlike traditional database models, Neo4j's graph approach offers distinct advantages for connected data: - -| Database Type | Core Concept | RushDB Analogy | -| -------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- | -| **Relational DB** | Tables with rows and columns | A table would be a label, a row would be a record, but relationships would require complex JOIN operations | -| **Document DB** | Collections of JSON documents | Each JSON document would be a record, but connecting documents requires explicit reference fields | -| **Graph DB (Neo4j)** | Nodes and relationships | Records are nodes with properties, and relationships are first-class citizens that connect related data | - -In Neo4j, relationships are physical connections in the database, not just foreign key references, enabling: - -- Traversing connections without costly JOIN operations -- Discovering patterns across different data types -- Modeling complex, interconnected data naturally - -## Neo4j Foundation - -Neo4j is responsible for all record and property data in RushDB, providing: - -- High-performance graph traversals -- ACID-compliant transactions -- Property graph model flexibility -- Scalable data storage and retrieval - -The [APOC](https://neo4j.com/labs/apoc/) plugin extends Neo4j with JSON conversion (`apoc.convert.*`), map utilities (`apoc.map.*`), and collection helpers (`apoc.coll.*`) that RushDB relies on for property storage and updates. Vector similarity search is handled entirely by **Neo4j's native vector index** — RushDB creates a `VECTOR INDEX` on embedding relationships and ranks filtered candidates with `vector.similarity.cosine()`. No GDS plugin is required. RushDB supports Neo4j 2026.01.4 and newer. - -## SQL Foundation - -Dashboard entities — users, workspaces, projects, and API tokens — are stored in a SQL database managed by [Drizzle ORM](https://orm.drizzle.team/). This separation keeps operational metadata out of the graph and allows standard relational tooling (migrations, studio, backups) to be used for account management. - -| Environment | Database | Configuration | -| ----------------- | -------------------- | ----------------------------------------------------- | -| Local development | SQLite (`rushdb.db`) | `SQL_DB_TYPE=sqlite` | -| Production | PostgreSQL | `SQL_DB_TYPE=postgres`, `SQL_DB_URL=postgresql://...` | - -Schema migrations are applied automatically on startup. - -## Data Overhead - -Each record in RushDB (a meaningful key-value data piece) is extended with several internal properties that enable advanced functionality: - -| Internal Key | Client Representation | Description | -| ----------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `__RUSHDB__KEY__ID__` | `__id` | UUIDv7 that enables lexicographic ordering without relying on user-defined fields like `createdAt`. RushDB SDKs support converting `__id` to timestamp and ISO8601 date. For more details, see [UUIDv7 specification](https://www.ietf.org/archive/id/draft-peabody-uuid-v7-01.html). | -| `__RUSHDB__KEY__PROPERTIES__META__` | `__proptypes` | Stringified meta-object holding the types of data in the current record, e.g., `{ name: "string", active: "boolean", ... }` | -| `__RUSHDB__KEY__LABEL__` | `__label` | Record Label identifier. Every record has two labels: a default one (`__RUSHDB__LABEL__RECORD__`) and a user-defined one that is searchable. Currently, RushDB allows only one custom label per record, and it is required by default. For more details about labels, see [Labels](../concepts/labels). | -| `__RUSHDB__KEY__PROJECT__ID__` | `__projectId` | Project identifier for multitenancy isolation. This property is never exposed to clients via UI or API. | - -## Data Structure - -RushDB organizes data in a graph structure that represents both records and properties as first-class entities: - -```mermaid -graph TD - subgraph Records - A[":__RUSHDB__LABEL__RECORD__ :Car"] - B[":__RUSHDB__LABEL__RECORD__ :Engine"] - F[":__RUSHDB__LABEL__RECORD__ :Motorcycle"] - G[":__RUSHDB__LABEL__RECORD__ :Engine"] - end - subgraph Properties - C[":__RUSHDB__LABEL__PROPERTY__
name: color"] - D[":__RUSHDB__LABEL__PROPERTY__
name: year"] - E[":__RUSHDB__LABEL__PROPERTY__
name: power"] - end - - A -->|"__RUSHDB__RELATION__DEFAULT__"| B - F -->|"__RUSHDB__RELATION__DEFAULT__"| G - - C -->|"__RUSHDB__RELATION__VALUE__"| A - C -->|"__RUSHDB__RELATION__VALUE__"| F - D -->|"__RUSHDB__RELATION__VALUE__"| A - E -->|"__RUSHDB__RELATION__VALUE__"| B - E -->|"__RUSHDB__RELATION__VALUE__"| G -``` - -In this structure: - -| Graph Element | Internal Label | Description | -| ----------------- | ------------------------------- | ---------------------------------------------------------------------------------- | -| Records | `__RUSHDB__LABEL__RECORD__` | Nodes that represent user-defined entities (Car, Engine, Motorcycle) | -| Properties | `__RUSHDB__LABEL__PROPERTY__` | Nodes that represent data attributes with a "name" field storing the property name | -| Value Relations | `__RUSHDB__RELATION__VALUE__` | Edges connecting properties to their records (the property → record direction) | -| Default Relations | `__RUSHDB__RELATION__DEFAULT__` | Edges connecting related records (like Car to Engine) | - -This structure allows efficient traversals across related records while maintaining property-based connections that can reveal relationships between otherwise unrelated entities. - -## Property Graph Model - -RushDB implements a unique property graph model where properties are first-class citizens: - -```mermaid -graph LR - subgraph Properties - B[":__RUSHDB__LABEL__PROPERTY__
name: color
type: string"] - end - subgraph Records - A[":__RUSHDB__LABEL__RECORD__ :Car"] - C[":__RUSHDB__LABEL__RECORD__ :Flower"] - end - B -->|__RUSHDB__RELATION__VALUE__| A - B -->|__RUSHDB__RELATION__VALUE__| C -``` - -Properties interconnect records through a unique set of field name and type. For example, both a `Car` record and a `Flower` record can share a common property `color:string`. This approach: - -1. Enables performant queries across different record types -2. Facilitates the discovery of hidden insights in data -3. Creates a natural graph structure that leverages Neo4j's native traversal capabilities - -Properties are not shared amongst projects (database instances), ensuring complete isolation in multi-tenant environments. - -## Data Types - -RushDB supports a wide range of data types to accommodate diverse data needs and provide a flexible environment for your applications. Below is a comprehensive list of the supported data types along with their descriptions: - -### String - -This data type is used for any textual information and can hold text of unlimited length. - -### Number - -This data type accommodates both floating-point numbers and integers. For instance, it can handle values like -`-120.209817` (a float) or `42` (an integer). - -### Datetime - -This data type adheres to the ISO 8601 format, including timezones. For example: `2012-12-21T18:29:37Z`. - -### Boolean - -This data type can only have two possible values: `true` or `false`. - -### Null - -This data type has only one possible value: `null`. - ---- - -### Arrays - -In essence, RushDB supports all the data types that JSON does. However, when it comes to arrays (or Lists), RushDB can indeed -hold them as **Property** values, but it's important to note that it can only store consistent values within those -arrays. To learn more, check out the [Properties](../concepts/properties) section. - -> **Note:** Every data type mentioned above supports an array representation. - -Here are some valid examples: - -- `["apple", "banana", "carrot"]` - good -- `[null, null, null, null, null]` - weird, but works fine 🤔 -- `[4, 8, 15, 16, 23, 42]` - works as well -- `["2023-09-17T02:47:54+04:00", "1990-08-18T04:35:00+05:00"]` - also good -- `[true, false, true, false, true]` - love is an answer (🌼) - -### Type Handling - -When records are imported into RushDB, data types are automatically inferred and stored in the `__RUSHDB__KEY__PROPERTIES__META__` internal field (exposed to clients as `__proptypes`). This metadata is crucial for maintaining type consistency and enabling efficient property-based connections across the graph. - -To learn more about how RushDB uses data types for property values and type inference during data import, see [REST API - Import Data](../rest-api/records/import-data). - -## Data Import Mechanism - -RushDB applies a breadth-first search (BFS) algorithm to parse JSON tree structures, enhancing each flat level with: - -1. A unique ID (`__id`), automatically generated or provided by the user for top-level records -2. Labels (`__label`), either explicitly provided by the user for top-level records or derived from parent keys for nested objects -3. Type inference, automatically suggesting data types for all properties -4. Relationship establishment, connecting nested records with `__RUSHDB__RELATION__DEFAULT__` relationships - -This approach allows for intuitive transformation of hierarchical JSON data into graph structures without requiring users to understand the underlying graph model. - -Example of JSON to graph transformation: - -```json -{ - "car": { - "make": "Tesla", - "model": "Model 3", - "engine": { - "power": 283, - "type": "electric" - } - } -} -``` - -Which transforms into the following graph structure: - -```mermaid -graph TD - subgraph Records - A[":__RUSHDB__LABEL__RECORD__ :car
__id: 01968aa4-22c1-781a-8e8c-8fe6be6c3fd4
__label: car
make: Tesla
model: Model 3
__proptypes: {make: string, model: string}"] - B[":__RUSHDB__LABEL__RECORD__ :engine
__id: 01968aa4-74af-73e4-984d-3888d63ec72e
__label: engine
power: 283
type: electric
__proptypes: {power: number, type: string}"] - end - - subgraph Properties - C[":__RUSHDB__LABEL__PROPERTY__
name: make
type: string"] - D[":__RUSHDB__LABEL__PROPERTY__
name: model
type: string"] - E[":__RUSHDB__LABEL__PROPERTY__
name: power
type: number"] - F[":__RUSHDB__LABEL__PROPERTY__
name: type
type: string"] - end - - A -->|"__RUSHDB__RELATION__DEFAULT__"| B - - C -->|"__RUSHDB__RELATION__VALUE__"| A - D -->|"__RUSHDB__RELATION__VALUE__"| A - E -->|"__RUSHDB__RELATION__VALUE__"| B - F -->|"__RUSHDB__RELATION__VALUE__"| B -``` - -In this representation: - -- Records are nodes with the label `__RUSHDB__LABEL__RECORD__` plus a user-defined label (car, engine) -- Records store the actual values of properties directly as node attributes -- Records also store `__proptypes` metadata about property types -- Properties are nodes with the single label `__RUSHDB__LABEL__PROPERTY__` and contain only the name and type fields (not values) -- Nested objects become connected records with `__RUSHDB__RELATION__DEFAULT__` relationships -- Properties are connected to their records via `__RUSHDB__RELATION__VALUE__` relationships (the property → record direction) - -The JSON representation of these records as stored in the database would look like: - -```json -[ - { - "__RUSHDB__KEY__ID__": "01968aa4-22c1-781a-8e8c-8fe6be6c3fd4", - "__RUSHDB__KEY__LABEL__": "car", - "__RUSHDB__KEY__PROJECT__ID__": "01968aa4-4225-7833-ba60-2f5e4383bf1b", - "__RUSHDB__KEY__PROPERTIES__META__": "{\"make\":\"string\",\"model\":\"string\"}", - "make": "Tesla", - "model": "Model 3" - }, - { - "__RUSHDB__KEY__ID__": "01968aa4-74af-73e4-984d-3888d63ec72e", - "__RUSHDB__KEY__LABEL__": "engine", - "__RUSHDB__KEY__PROJECT__ID__": "01968aa4-4225-7833-ba60-2f5e4383bf1b", - "__RUSHDB__KEY__PROPERTIES__META__": "{\"power\":\"number\",\"type\":\"string\"}", - "power": 283, - "type": "electric" - } -] -``` - -When these records are returned through RushDB's API or SDKs, the internal keys are transformed to their client-friendly aliases: - -```json -[ - { - "__id": "01968aa4-22c1-781a-8e8c-8fe6be6c3fd4", - "__label": "car", - "__proptypes": { "make": "string", "model": "string" }, - "make": "Tesla", - "model": "Model 3" - }, - { - "__id": "01968aa4-74af-73e4-984d-3888d63ec72e", - "__label": "engine", - "__proptypes": { "power": "number", "type": "string" }, - "power": 283, - "type": "electric" - } -] -``` - -Note that the `__projectId` field is never exposed to clients via the API or SDKs as noted in the Data Overhead section. - -Each of these records is also connected to Property nodes which define the metadata for their fields, but those Property nodes don't store the actual values. - -## Database Indexes and Constraints - -When RushDB connects to Neo4j, it automatically creates several indexes and constraints on graph nodes to ensure data integrity and optimize query performance. - -> **Note:** Users, workspaces, projects, and API tokens are stored in the SQL layer and are not represented as Neo4j nodes. Their uniqueness and integrity are enforced by SQL constraints and Drizzle ORM migrations. - -### Core Constraints - -The following uniqueness constraints are created on graph nodes: - -| Constraint Name | Node Label | Property | Description | -| ------------------------ | ----------------------------- | --------------------- | ------------------------------------------ | -| `constraint_record_id` | `__RUSHDB__LABEL__RECORD__` | `__RUSHDB__KEY__ID__` | Ensures each record has a unique ID | -| `constraint_property_id` | `__RUSHDB__LABEL__PROPERTY__` | `id` | Ensures each property node has a unique ID | - -### Performance Indexes - -The following indexes are created to optimize query performance: - -| Index Name | Node Label | Properties | Description | -| ------------------------- | ----------------------------- | --------------------------------------- | -------------------------------------------------------------- | -| `index_record_id` | `__RUSHDB__LABEL__RECORD__` | `__RUSHDB__KEY__ID__` | Speeds up record lookups by ID | -| `index_record_projectid` | `__RUSHDB__LABEL__RECORD__` | `__RUSHDB__KEY__PROJECT__ID__` | Enables fast filtering of records by project | -| `index_property_name` | `__RUSHDB__LABEL__PROPERTY__` | `name` | Enables fast property lookups by name | -| `index_property_mergerer` | `__RUSHDB__LABEL__PROPERTY__` | `name`, `type`, `projectId`, `metadata` | Optimizes property node merging operations during data imports | - -These indexes and constraints are essential for RushDB's performance and data integrity, particularly when dealing with large datasets and complex queries across the property graph model. They ensure that: - -1. Record IDs are always unique within the graph database -2. Project isolation is maintained in multi-tenant environments -3. Property lookups are efficient, especially during graph traversals and joins - -Learn more at [REST API - Import Data](../rest-api/records/import-data) or through the language-specific SDKs: - -- [TypeScript SDK](../typescript-sdk/records/import-data) -- [Python SDK](../python-sdk/records/import-data) - -## Performance Considerations - -This approach is carefully designed to: - -- Enable efficient indexing and querying -- Support advanced graph traversals and pattern matching -- Facilitate semantic search and similarity re-ranking with minimal computational cost - -By structuring data this way, RushDB achieves a balance between storage overhead and query performance, optimizing for use cases that require both traditional database operations and advanced graph analytics capabilities. diff --git a/docs/docs/concepts/transactions.mdx b/docs/docs/concepts/transactions.mdx deleted file mode 100644 index ee421ee4..00000000 --- a/docs/docs/concepts/transactions.mdx +++ /dev/null @@ -1,380 +0,0 @@ ---- -sidebar_position: 7 ---- - -import Tabs from '@site/src/components/LanguageTabs' -import TabItem from '@theme/TabItem' - -# Transactions - -In RushDB, Transactions provide a mechanism to group multiple database operations into a single atomic unit of work. They ensure data consistency by guaranteeing that either all operations within the transaction succeed, or none of them do. - -## How It Works - -Transactions in RushDB are built on Neo4j's native transaction capabilities, providing ACID guarantees: - -- **Atomicity**: All operations within a transaction either succeed completely or fail completely, with no partial changes. -- **Consistency**: Transactions transform the database from one valid state to another, maintaining data integrity. -- **Isolation**: Concurrent transactions do not interfere with each other, ensuring data consistency. -- **Durability**: Once a transaction is committed, changes are permanent even in case of system failure. - -For example: - - - - ```typescript - // Start a transaction - const tx = await db.tx.begin({ ttl: 10000 }); - -try { -// Create a new user record within the transaction -const user = await db.records.create( -{ -label: 'User', -data: { -name: 'Alice Smith', -email: 'alice@example.com', -emailConfirmed: false -} -}, -tx -); - - // Create a related profile record within the same transaction - const profile = await db.records.create( - { - label: "Profile", - data: { - bio: "Software engineer", - joinDate: new Date().toISOString() - } - }, - tx - ); - - // Create a relationship between the records within the same transaction - await db.records.attach( - { - source: user, - target: profile, - options: { - type: 'HAS_PROFILE' - } - }, - tx - ); - - // Commit the transaction to make all changes permanent - await tx.commit(); - // or await db.tx.commit(tx); - -} catch (error) { -// If any operation fails, roll back the entire transaction -await tx.rollback(); -// or await db.tx.rollback(tx); -throw error; -} - -```` - - - ```python - # Start a transaction - tx = db.tx.begin(ttl=10000) - - try: - # Create a new user record within the transaction - user = db.records.create( - label="User", - data={ - "name": "Alice Smith", - "email": "alice@example.com", - "emailConfirmed": False - }, - transaction=tx - ) - - # Create a related profile record within the same transaction - profile = db.records.create( - label="Profile", - data={ - "bio": "Software engineer", - "joinDate": datetime.now().isoformat() - }, - transaction=tx - ) - - # Create a relationship between the records within the same transaction - db.records.attach( - source=user, - target=profile, - options={ - "type": "HAS_PROFILE" - }, - transaction=tx - ) - - # Commit the transaction to make all changes permanent - tx.commit() - # or db.tx.commit(tx) - except Exception as error: - # If any operation fails, roll back the entire transaction - tx.rollback() - # or db.tx.rollback(tx) - raise error - ``` - - - -## Transaction Lifecycle - -Each transaction in RushDB follows a clear lifecycle: - -1. **Creation**: A transaction is initiated with an optional Time-To-Live (TTL) parameter -2. **Operation Phase**: Multiple database operations are performed using the transaction ID -3. **Termination**: The transaction is explicitly committed to make changes permanent or rolled back to discard all changes -4. **Automatic Cleanup**: If neither committed nor rolled back within the TTL, the transaction is automatically rolled back - -## Internal Structure - -Built on Neo4j's transaction management system, RushDB transactions maintain several internal states: - -| State | Description | -|-------|-------------| -| Active | Transaction is open and can accept operations | -| Committed | Transaction has been successfully completed | -| Rolled Back | Transaction has been explicitly or automatically reverted | -| Timed Out | Transaction exceeded its TTL and was automatically rolled back | - -Internally, RushDB maintains a transaction registry that: -1. Tracks all active transactions -2. Monitors their TTL -3. Maps transaction IDs to internal Neo4j transaction objects -4. Manages transaction cleanup and resource release - -For more information about the underlying storage system, see [Storage](../concepts/storage). - -## Use Cases - -Transactions are particularly valuable in several scenarios: - -### Complex Data Operations - -When creating interconnected data structures, transactions ensure that all components are created successfully or not at all: - -```mermaid -graph TD - A[Create User] -->|Success| B[Create Profile] - B -->|Success| C[Create Address] - C -->|Success| D[Create Relationships] - D -->|Success| E[Commit Transaction] - - A -->|Failure| F[Rollback] - B -->|Failure| F - C -->|Failure| F - D -->|Failure| F -```` - -This approach ensures that complex data structures are properly maintained, with [Records](../concepts/records) and their [Relationships](../concepts/relationships) remaining consistent. - -### Concurrent Operations - -When multiple users or services access the same data simultaneously, transactions maintain data consistency: - - - - ```typescript - // Service 1 - const tx1 = await db.tx.begin(); - try { - const record = await db.records.findById(recordId, tx1); - await db.records.update( - { - target: record, - label: record.label, - data: { status: "processing" } - }, - tx1 - ); - await tx1.commit(); - } catch (error) { - await tx1.rollback(); - } - -// Service 2 (concurrent) -const tx2 = await db.tx.begin(); -try { -const record = await db.records.findById(recordId, tx2); -// Will see the original record state until tx1 is committed -await tx2.commit() -} catch (error) { -await tx2.rollback() -} - -```` - - - ```python - # Service 1 - tx1 = db.tx.begin() - try: - record = db.records.find_by_id(record_id, transaction=tx1) - db.records.update( - target=record, - label=record.label(), - data={"status": "processing"}, - transaction=tx1 - ) - tx1.commit() - except Exception as error: - tx1.rollback() - - # Service 2 (concurrent) - tx2 = db.tx.begin() - try: - record = db.records.find_by_id(record_id, transaction=tx2) - # Will see the original record state until tx1 is committed - tx2.commit() - except Exception as error: - tx2.rollback() - ``` - - - -Transactions help prevent race conditions when multiple operations might affect the same [Records](../concepts/records) or [Properties](../concepts/properties). - -### Data Migrations - -When upgrading data structures or transforming records, transactions ensure that data integrity is maintained: - - -```typescript -const tx = await db.tx.begin({ ttl: 30000 }); // Longer TTL for migrations -try { - const users = await db.records.find({ labels: ["User"] }, tx); - - for (const user of users) { - // Create new format record - await db.records.create( - { - label: "Person", - data: { - fullName: `${user.firstName} ${user.lastName}`, - email: user.email, - migratedFrom: user.__id - } - }, - tx - ); - } - - await tx.commit(); -} catch (error) { - await tx.rollback(); - console.error("Migration failed:", error); -} -```` - - - - ```python - tx = db.tx.begin(ttl=30000) # Longer TTL for migrations - try: - users = db.records.find({"labels": ["User"]}, transaction=tx) - - for user in users: - # Create new format record - db.records.create( - label="Person", - data={ - "fullName": f"{user.get('firstName')} {user.get('lastName')}", - "email": user.get('email'), - "migratedFrom": user.get('__id') - }, - transaction=tx - ) - - tx.commit() - except Exception as error: - tx.rollback() - print(f"Migration failed: {error}") - ``` - - - - -During migrations, transactions ensure that [Labels](../concepts/labels) and [Properties](../concepts/properties) are consistently updated across related records. - -## Time-To-Live (TTL) - -RushDB transactions include a configurable TTL mechanism to prevent hanging transactions: - -- **Default**: 5000ms (5 seconds) -- **Maximum**: 30000ms (30 seconds) -- **Purpose**: Automatically rolls back transactions that aren't explicitly committed or rolled back within the specified time -- **Recommendation**: Set TTL based on expected operation duration plus a reasonable buffer - -## Transaction Limitations - -While transactions provide powerful data consistency guarantees, they come with certain limitations: - -1. **Resource Consumption**: Active transactions consume database resources, particularly memory -2. **Performance Impact**: Very long-running transactions can impact overall database performance -3. **TTL Constraints**: Maximum TTL is capped at 30 seconds to prevent resource exhaustion -4. **Isolation Level**: RushDB uses Neo4j's default read-committed isolation level - -See [Storage](../concepts/storage) for more details on how database resources are managed. - -## Best Practices - -To effectively use transactions in RushDB: - -1. **Keep transactions short**: Minimize the number and duration of operations within a transaction -2. **Set appropriate TTL**: Choose a TTL that provides enough time for operations to complete without being unnecessarily long -3. **Explicit termination**: Always explicitly commit or rollback transactions rather than relying on automatic TTL-based rollback -4. **Error handling**: Implement proper error handling with rollback in catch blocks -5. **Avoid nested transactions**: Instead of nesting transactions, design workflows to use a single transaction level -6. **Batch operations**: For bulk operations, consider batching changes into multiple smaller transactions - -When working with complex data models, refer to [Records](../concepts/records) and [Relationships](../concepts/relationships) documentation to understand how transactions affect your data structures. - -## Integration with Neo4j - -RushDB's transaction system leverages Neo4j's native transaction capabilities while adding: - -1. Client-friendly transaction IDs -2. Configurable TTL with automatic cleanup -3. Cross-platform SDK integration -4. HTTP API support via transaction headers - -This provides the robustness of Neo4j's proven transaction system with the ease of use of RushDB's modern API design. - -## Implementation Reference - -Each interface covers the full transaction API — pick the one that fits your stack: - -
- - TypeScript SDK - db.tx.begin · commit · rollback - - - Python SDK - db.tx.begin · commit · rollback - - - REST API - - POST /transactions · commit · rollback - - -
diff --git a/docs/docs/connect/agents.mdx b/docs/docs/connect/agents.mdx new file mode 100644 index 00000000..b68c33eb --- /dev/null +++ b/docs/docs/connect/agents.mdx @@ -0,0 +1,74 @@ +--- +sidebar_position: 3 +title: Agents +displayed_sidebar: docs +--- + +# Use RushDB with AI Agents + +RushDB gives agents a structured data layer, a traversable graph, and persistent memory. Choose the shortest setup that matches your agent. + +## Fastest Setup: Give the Agent the Guide + +For an MCP-capable agent, send: + +``` +Fetch https://rushdb.com/agent-setup and follow the instructions exactly. +``` + +The machine-readable setup guide walks the agent through MCP connection, schema discovery, memory labels, nested record linking, recall patterns, and semantic search. + +## Coding Assistant: MCP + Skills + Memory + +For Claude Desktop, Cursor, VS Code Copilot, OpenClaw, or another local coding agent: + +1. [Connect the local MCP server](/connect/mcp#local-mcp-api-key). +2. [Install RushDB Agent Skills](/connect/skills). +3. Start a new agent session. +4. Send: + +``` +Set up RushDB as my persistent memory layer. +``` + +The agent calls `getOntologyMarkdown`, creates a `SESSION`, and validates recall. For the full bootstrap prompt and recommended labels, follow the [Agent Memory Quickstart](/build/agent-memory/quickstart). + +## Custom Agent Harness + +For an application-controlled agent, choose the interface based on where orchestration lives: + +| Interface | Use it when | +| -------------- | ------------------------------------------------------------------------ | +| MCP tools | Your framework already supports MCP tool discovery and invocation | +| TypeScript SDK | Your harness runs in Node.js, the browser, or an edge-compatible runtime | +| Python SDK | Your harness runs in Python | +| REST API | You need language-neutral HTTP integration | + +[Connect with an SDK or REST →](/connect/sdks) + +At the start of each agent session: + +1. Call `getOntologyMarkdown` to discover the live schema. +2. Recall the latest `SESSION` and relevant `DECISION`, `TASK`, or `PREFERENCE` records. +3. Create a new `SESSION`. + +Before the session ends: + +1. Store decisions, tasks, observations, and artifacts. +2. Link them to the session through nested JSON or relationships. +3. Confirm the stored summary. + +## Verify + +Ask your agent: + +> Search RushDB for `SESSION` records. Show the most recent one and summarize any linked decisions or tasks. + +If you have not bootstrapped memory yet, use the [Agent Memory Quickstart](/build/agent-memory/quickstart) first. + +## Next Steps + +- [Agent Memory Quickstart](/build/agent-memory/quickstart) +- [Schema Self-Awareness](/build/agent-memory/schema-self-awareness) +- [Agent-Safe Query Planning](/tutorials/agent-safe-query-planning) +- [Using RushDB Agent Skills in OpenClaw](/tutorials/agent-skills-with-openclaw) diff --git a/docs/docs/connect/index.mdx b/docs/docs/connect/index.mdx new file mode 100644 index 00000000..9a7eb92c --- /dev/null +++ b/docs/docs/connect/index.mdx @@ -0,0 +1,125 @@ +--- +sidebar_position: 1 +title: Connect RushDB +displayed_sidebar: docs +--- + +# Connect RushDB + +Start with the path that matches how you work. Each option gets you to a working RushDB connection first, then points to the next useful workflow. + +| Goal | Fastest path | Time | +| ---------------------------------------------------- | ------------------------------------------------- | ------ | +| Use RushDB from ChatGPT or Claude.ai | [Hosted MCP connector](#hosted-mcp-connector) | ~1 min | +| Give Cursor, Claude Desktop, or VS Code RushDB tools | [Local MCP server](#local-mcp-server) | ~3 min | +| Teach an AI agent correct RushDB workflows | [Install Agent Skills](#install-agent-skills) | ~2 min | +| Add persistent memory to an agent | [Bootstrap agent memory](#bootstrap-agent-memory) | ~5 min | +| Build an application or data pipeline | [Use an SDK or REST](#use-an-sdk-or-rest) | ~5 min | + +Need an API key for a local or application connection? [Create one in the dashboard](/deploy/get-api-key). + +--- + +## Hosted MCP Connector + +For ChatGPT, Claude.ai, and other web-based MCP clients, add this URL as a connector: + +``` +https://mcp.rushdb.com/mcp +``` + +Sign in with your RushDB account and choose the project to expose. No local installation or API key is required. + +**Verify:** + +> Call `getOntologyMarkdown` and show me what labels exist in my RushDB project. + +[Set up a hosted MCP connector →](/connect/mcp#hosted-oauth-no-installation) + +--- + +## Local MCP Server + +For Claude Desktop, Cursor, VS Code, and other local MCP clients, run the MCP server with your project API key: + +```json +{ + "mcpServers": { + "rushdb": { + "command": "npx", + "args": ["-y", "@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "your-api-key-here" + } + } + } +} +``` + +Restart your client, then run the same ontology verification prompt. + +[Choose your local MCP client →](/connect/mcp#local-mcp-api-key) + +--- + +## Install Agent Skills + +Install RushDB Agent Skills so your assistant knows how to query, model data, and manage persistent memory: + +```bash +npx skills add rush-db/rushdb --path packages/skills +``` + +Start a new agent session after installation so the skills are discovered. + +[Install and verify Agent Skills →](/connect/skills) + +--- + +## Bootstrap Agent Memory + +After MCP and Skills are installed, give your agent the canonical machine-readable setup guide: + +``` +Fetch https://rushdb.com/agent-setup and follow the instructions exactly. +``` + +The agent discovers the live schema, creates a `SESSION`, imports existing preferences when available, and validates recall. + +[Bootstrap persistent memory →](/build/agent-memory/quickstart) + +--- + +## Use an SDK or REST + +For application code, use the TypeScript SDK, Python SDK, or JSON REST API. The first useful test is a nested import: RushDB creates the records and their relationships in one request. + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "PROJECT", + "data": { + "name": "RushDB adoption", + "TASK": [ + {"title": "Connect RushDB", "status": "done"}, + {"title": "Query linked records", "status": "pending"} + ] + } + }' +``` + +[Connect with TypeScript, Python, or REST →](/connect/sdks) + +--- + +## Choose What to Build + +| Use case | Guide | +| --------------------------------- | --------------------------------------------------------- | +| Persistent agent memory | [Agent Memory Quickstart](/build/agent-memory/quickstart) | +| Semantic search over your data | [Semantic Search](/build/ai-search/semantic-search) | +| Graph-enriched retrieval | [GraphRAG Tutorial](/tutorials/graphrag) | +| Explore an unknown dataset safely | [Discovery Queries](/tutorials/discovery-queries) | +| Design labels and relationships | [Data Modeling](/build/data/import-data) | diff --git a/docs/docs/connect/mcp.mdx b/docs/docs/connect/mcp.mdx new file mode 100644 index 00000000..b1313697 --- /dev/null +++ b/docs/docs/connect/mcp.mdx @@ -0,0 +1,191 @@ +--- +sidebar_position: 2 +title: MCP Server +displayed_sidebar: docs +--- + +# MCP Server + +Give an AI assistant direct access to your RushDB project without writing integration code. RushDB supports hosted OAuth connections for web assistants and a local `npx` server for desktop and coding tools. + +| Mode | Best for | Setup | +| --------------------------------------------- | ------------------------------- | ----------------------- | +| [Hosted OAuth](#hosted-oauth-no-installation) | ChatGPT, Claude.ai, web clients | Add one URL and sign in | +| [Local MCP](#local-mcp-api-key) | Claude Desktop, Cursor, VS Code | Add an MCP config block | + +Reference: [MCP Server Reference](/reference/mcp/) · [All MCP Tools](/reference/mcp/tools) + +--- + +## Hosted OAuth: No Installation + +Use this endpoint: + +``` +https://mcp.rushdb.com/mcp +``` + +### ChatGPT + +1. Open **Settings → Connectors → Add connector**. +2. Enter `https://mcp.rushdb.com/mcp`. +3. Sign in with your RushDB account. +4. Choose the project to expose. + +### Claude.ai + +1. Open **Settings → Integrations → Add integration**. +2. Enter `https://mcp.rushdb.com/mcp`. +3. Authorize with your RushDB account. +4. Choose the project to expose. + +:::caution +Always include `/mcp`. The root domain does not serve the MCP protocol. +::: + +--- + +## Local MCP: API Key + +Get a [project API key](/deploy/get-api-key), add the relevant configuration, then restart your client. + +### Claude Desktop + +Edit `~/Library/Application Support/Claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "rushdb": { + "command": "npx", + "args": ["-y", "@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "your-api-key-here" + } + } + } +} +``` + +### Cursor + +Add `.cursor/mcp.json` in your project: + +```json +{ + "mcpServers": { + "rushdb": { + "command": "npx", + "args": ["-y", "@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "your-api-key-here" + } + } + } +} +``` + +### VS Code: Copilot Agent Mode + +Add `.vscode/mcp.json`: + +```json +{ + "servers": { + "rushdb": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "your-api-key-here" + } + } + } +} +``` + +### Self-Hosted Instance + +Add `RUSHDB_API_URL` to the `env` block: + +```json +{ + "env": { + "RUSHDB_API_KEY": "your-api-key-here", + "RUSHDB_API_URL": "https://your-rushdb.example.com/api/v1" + } +} +``` + +--- + +## Verify the Connection + +Ask your assistant: + +> Call `getOntologyMarkdown` and show me what labels exist in my RushDB project. + +A new project returns an empty ontology. That still confirms the connection works. + +--- + +## Mandatory Workflow + +RushDB MCP exposes a discovery-first workflow so agents query the schema that exists instead of guessing labels, fields, or operators: + +1. Call `getOntologyMarkdown` at the start of a session. It returns labels, property names and types, value ranges, and relationships. +2. Classify the request as a listing, aggregation, traversal, semantic search, or mutation. +3. Call `getSearchQuerySpec` before building queries with dates, metrics, `groupBy`, relationship traversal, or vector search. +4. Use the discovered label and property names exactly as returned. Labels are case-sensitive. + +Clients with MCP Prompts support can fetch `rushdb.queryBuilder` at session start. Other clients can call `getQueryBuilderPrompt` for the same instructions. + +## Local Configuration Reference + +| Variable | Required | Default | Description | +| ---------------- | -------- | ------------------------------- | ------------------------------------------------ | +| `RUSHDB_API_KEY` | yes | — | RushDB project API key | +| `RUSHDB_API_URL` | no | `https://api.rushdb.com/api/v1` | Override for self-hosted or staging environments | + +:::tip Security +Treat `RUSHDB_API_KEY` like a password. Inject it through your MCP client or an OS keychain, and do not commit it to version control. +::: + +## Essential Tools + +| Need | Start with | +| ------------------------------- | --------------------------------------------------------------------------------------- | +| Discover a project schema | `getOntologyMarkdown`, `getOntology` | +| Build a precise query | `getSearchQuerySpec`, `findRecords` | +| Read or write records | `getRecord`, `createRecord`, `updateRecord`, `setRecord` | +| Connect records | `findRelationships`, `attachRelation`, `detachRelation` | +| Work in batches | `bulkCreateRecords`, `bulkDeleteRecords`, `exportRecords` | +| Review inferred graph structure | `listRelationshipPatterns`, `analyzeRelationshipPatterns`, `approveRelationshipPattern` | +| Add semantic search | `findEmbeddingIndexes`, `createEmbeddingIndex`, `semanticSearch` | + +Each MCP tool publishes its input schema to the connected client. See [All MCP Tools](/reference/mcp/tools) for the complete catalog and use the client tool inspector when you need the full JSON schema. + +## Troubleshooting + +| Symptom | Check | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Hosted connector cannot discover actions | Confirm the URL is `https://mcp.rushdb.com/mcp`, including `/mcp`, then remove and recreate the connector. | +| Local MCP reports a missing API key | Set `RUSHDB_API_KEY` in the MCP client's `env` block and restart the client. | +| Local MCP cannot reach RushDB | Verify `RUSHDB_API_URL`, or remove it to restore the hosted default. | +| A query uses the wrong label or property | Call `getOntologyMarkdown` again and use the case-sensitive names it returns. | +| A structured query fails validation | Call `getSearchQuerySpec` and rebuild the query from the documented operators. | + +For OAuth diagnostics, verify that `/.well-known/openid-configuration` returns JSON, `/.well-known/jwks.json` returns keys, and `/mcp` responds as an MCP endpoint. + +--- + +## Make the Agent More Effective + +Install [RushDB Agent Skills](/connect/skills) after MCP connection. Skills teach the agent discovery-first querying, graph modeling, faceted search, and persistent memory patterns. + +## Next Steps + +- [Browse the complete MCP tools reference](/reference/mcp/tools) +- [Install Agent Skills](/connect/skills) +- [Bootstrap persistent agent memory](/build/agent-memory/quickstart) +- [Use RushDB with AI agents](/connect/agents) diff --git a/docs/docs/connect/sdks.mdx b/docs/docs/connect/sdks.mdx new file mode 100644 index 00000000..8d192792 --- /dev/null +++ b/docs/docs/connect/sdks.mdx @@ -0,0 +1,179 @@ +--- +sidebar_position: 4 +title: SDK & REST +displayed_sidebar: docs +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# SDK & REST + +Use the TypeScript SDK, Python SDK, or REST API when RushDB is part of your application, service, script, or data pipeline. + +The fastest useful test is a nested import followed by a search. Nested objects become linked records automatically. + +## Install, Connect, and Write + + + + +```bash +npm install @rushdb/javascript-sdk +``` + +```typescript +import RushDB from '@rushdb/javascript-sdk' + +const db = new RushDB('RUSHDB_API_KEY') + +await db.records.importJson({ + label: 'PROJECT', + data: { + name: 'RushDB adoption', + TASK: [ + { title: 'Connect RushDB', status: 'done' }, + { title: 'Query linked records', status: 'pending' } + ] + } +}) +``` + + + + +```bash +pip install rushdb +``` + +```python +from rushdb import RushDB + +db = RushDB("RUSHDB_API_KEY") + +db.records.create_many( + label="PROJECT", + data={ + "name": "RushDB adoption", + "TASK": [ + {"title": "Connect RushDB", "status": "done"}, + {"title": "Query linked records", "status": "pending"}, + ], + }, +) +``` + + + + +```bash +export RUSHDB_API_KEY=your-api-key + +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "PROJECT", + "data": { + "name": "RushDB adoption", + "TASK": [ + {"title": "Connect RushDB", "status": "done"}, + {"title": "Query linked records", "status": "pending"} + ] + } + }' +``` + + + + +## Verify with a Read + + + + +```typescript +const { data: projects, total } = await db.records.find({ + labels: ['PROJECT'], + where: { name: 'RushDB adoption' } +}) + +console.log({ total, projects }) +``` + + + + +```python +projects = db.records.find({ + "labels": ["PROJECT"], + "where": {"name": "RushDB adoption"}, +}) + +print(projects.total, projects.data) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["PROJECT"], + "where": {"name": "RushDB adoption"} + }' +``` + + + + +## Self-Hosted Instance + + + + +```typescript +const db = new RushDB('RUSHDB_API_KEY', { + url: 'https://your-rushdb.example.com/api/v1' +}) +``` + + + + +```python +db = RushDB("RUSHDB_API_KEY", url="https://your-rushdb.example.com") +``` + + + + +```bash +export RUSHDB_URL=https://your-rushdb.example.com + +curl -X POST "$RUSHDB_URL/api/v1/records/search" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"labels": ["PROJECT"]}' +``` + + + + +## Add Semantic Search + +Semantic search requires a one-time embedding index for the label and text property you want to search. After the first write/read flow works: + +1. [Create an embedding index](/build/ai-search/manage-indexes). +2. Wait until the index status is `ready`. +3. [Run semantic search](/build/ai-search/semantic-search). + +## Full References + +- [TypeScript / JavaScript reference](/reference/typescript/) +- [Python reference](/reference/python/) +- [REST API reference](/rest-api/introduction) +- [Store records](/build/data/store-records) +- [Import nested data](/build/data/import-data) diff --git a/docs/docs/connect/skills.mdx b/docs/docs/connect/skills.mdx new file mode 100644 index 00000000..d06e291e --- /dev/null +++ b/docs/docs/connect/skills.mdx @@ -0,0 +1,56 @@ +--- +sidebar_position: 1 +title: Agent Skills +displayed_sidebar: docs +--- + +# Agent Skills + +Install RushDB Agent Skills to teach compatible AI assistants how to query RushDB correctly, model graph data, build faceted search, and use RushDB as persistent memory. + +Skills complement the [MCP server](./mcp): MCP gives an agent runtime tools; Skills teach the agent when and how to use them. + +## Install + +For Agent Skills-compatible clients: + +```bash +npx skills add rush-db/rushdb --path packages/skills +``` + +Or install the npm package: + +```bash +npm install @rushdb/skills +``` + +Start a new agent session after installation so the skills are discovered. + +## Included Skills + +| Skill | What it enables | +| ------------------------ | ------------------------------------------------------------------------------------------------------------- | +| `rushdb-agent-memory` | Store sessions, decisions, tasks, and preferences; recall prior context | +| `rushdb-query-builder` | Build correct filters, traversals, aggregations, and semantic searches | +| `rushdb-data-modeling` | Design labels, properties, relationships, and nested JSON imports | +| `rushdb-faceted-search` | Build faceted filter interfaces from live property metadata | +| `rushdb-domain-template` | Design a schema for any domain through guided conversation — entities, relationships, and a bootstrap payload | + +## Verify + +After connecting the MCP server and starting a new session, ask: + +> List the installed RushDB skills. Then call `getOntologyMarkdown` and summarize the labels in my RushDB project. + +An empty ontology is valid for a new project. The important result is that the agent recognizes the RushDB skills and can call the MCP tool. + +## OpenClaw + +OpenClaw uses workspace-level skill installation. Follow the [OpenClaw walkthrough](../tutorials/agent-skills-with-openclaw#part-2--install-the-rushdb-skills-pack) for native `openclaw` and ClawHub commands. + +## Next Steps + +- [Connect the MCP server](./mcp) +- [Bootstrap persistent agent memory](../build/agent-memory/quickstart) +- [Use RushDB with AI agents](./agents) +- [Browse the skills source](https://github.com/rush-db/rushdb/tree/main/packages/skills) diff --git a/docs/docs/deploy/configuration/environment-variables.mdx b/docs/docs/deploy/configuration/environment-variables.mdx new file mode 100644 index 00000000..e62b2ba4 --- /dev/null +++ b/docs/docs/deploy/configuration/environment-variables.mdx @@ -0,0 +1,219 @@ +--- +slug: /deploy/config-env-variables +title: Environment Variables +description: Complete reference for all RushDB environment variables. +sidebar_position: 1 +displayed_sidebar: docs +tags: [Configuration, Environment Variables, Self-Hosted] +--- + +# Environment Variables + +All configuration is done via environment variables. The authoritative template is [`platform/core/.env.example`](https://github.com/rush-db/rushdb/blob/main/platform/core/.env.example) in the repository. + +--- + +## Required (Self-Hosted) + +These four variables must be set or RushDB will not start correctly. + +| Variable | Default | Description | +| ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `RUSHDB_SELF_HOSTED` | — | Must be `true` to enable self-hosted mode | +| `RUSHDB_LOGIN` | — | Admin account username | +| `RUSHDB_PASSWORD` | — | Admin account password | +| `RUSHDB_AES_256_ENCRYPTION_KEY` | — | **Exactly 32 characters.** Used for AES-256 API token encryption and JWT signing. Do not change after initial setup without re-encrypting stored data. Generate: `openssl rand -hex 32` | + +--- + +## Neo4j + +| Variable | Default | Description | +| ---------------- | ----------------------- | ----------------------------------------------- | +| `NEO4J_URL` | `bolt://localhost:7687` | Bolt connection URL. Use `neo4j+s://` for Aura. | +| `NEO4J_USERNAME` | `neo4j` | Neo4j username | +| `NEO4J_PASSWORD` | — | **Required.** Neo4j password | + +See [Neo4j & Aura](/deploy/infra-neo4j) for setup details. + +--- + +## SQL Database + +Stores dashboard entities — users, workspaces, projects, tokens, OAuth sessions. + +| Variable | Default | Description | +| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------- | +| `SQL_DB_TYPE` | `sqlite` | `sqlite` (zero-config) or `postgres` | +| `SQL_DB_PATH` | `./rushdb.db` | Path to the SQLite file. Only used when `SQL_DB_TYPE=sqlite` | +| `SQL_DB_URL` | — | PostgreSQL connection URL, e.g. `postgresql://user:pass@host:5432/rushdb`. Required when `SQL_DB_TYPE=postgres` | +| `SQL_DB_SSL` | — | Set to `true` to enable SSL/TLS for the PostgreSQL connection | + +See [PostgreSQL / SQLite](/deploy/infra-database) for setup details. + +--- + +## Network & App + +| Variable | Default | Description | +| ----------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `RUSHDB_PORT` | `3000` | Port the API server listens on | +| `RUSHDB_DASHBOARD_URL` | `http://localhost:3005` | Public URL of the dashboard. Used in OAuth redirect URIs and password-reset email links | +| `RUSHDB_SERVE_STATIC` | `false` | Set to `true` to serve the frontend static assets from the same process | +| `RUSHDB_ALLOWED_LOGINS` | `[]` | JSON array of email addresses allowed to log in. Empty means all logins are allowed. Example: `'["alice@example.com","bob@example.com"]'` | + +--- + +## AI — Embeddings + +Enables vector similarity search. All four variables must be set together; omit them entirely to disable embedding indexes. + +| Variable | Default | Description | +| --------------------------------- | --------------------------- | -------------------------------------------------------------------------- | +| `RUSHDB_EMBEDDING_BASE_URL` | `https://api.openai.com/v1` | Base URL of an OpenAI-compatible embeddings endpoint | +| `RUSHDB_EMBEDDING_API_KEY` | — | Bearer token for the embedding provider | +| `RUSHDB_EMBEDDING_MODEL` | — | Model identifier, e.g. `text-embedding-3-small`, `qwen/qwen3-embedding-8b` | +| `RUSHDB_EMBEDDING_DIMENSIONS` | — | Number of dimensions the model outputs. Must match the model exactly. | +| `RUSHDB_EMBEDDING_BATCH_SIZE` | `500` | Records per batch during index backfill | +| `RUSHDB_EMBEDDING_MAX_RUNTIME_MS` | `50000` | Max milliseconds the backfill scheduler runs per tick | + +--- + +## AI — Relationship Suggestions (LLM) + +Powers automatic relationship candidate inference from your data ontology. + +| Variable | Default | Description | +| --------------------- | --------------------------- | ---------------------------------------------------------- | +| `RUSHDB_LLM_BASE_URL` | `https://api.openai.com/v1` | Base URL of an OpenAI-compatible chat completions endpoint | +| `RUSHDB_LLM_API_KEY` | — | Bearer token for the LLM provider | +| `RUSHDB_LLM_MODEL` | — | Model identifier, e.g. `gpt-4.1-mini` | + +--- + +## Email (SMTP) + +Required for email confirmation, password reset, and workspace invite flows. All four must be set together; omit to disable email features. + +| Variable | Default | Description | +| --------------- | ------- | ----------------------------------------------------------------- | +| `MAIL_HOST` | — | SMTP server hostname, e.g. `smtp.gmail.com` | +| `MAIL_USER` | — | SMTP auth username | +| `MAIL_PASSWORD` | — | SMTP auth password | +| `MAIL_FROM` | — | From address shown on outgoing emails, e.g. `noreply@example.com` | + +--- + +## OAuth Providers + +| Variable | Default | Description | +| ------------------ | ------- | ------------------------------ | +| `GOOGLE_CLIENT_ID` | — | Google OAuth 2.0 client ID | +| `GOOGLE_SECRET` | — | Google OAuth 2.0 client secret | +| `GH_CLIENT_ID` | — | GitHub OAuth app client ID | +| `GH_SECRET` | — | GitHub OAuth app client secret | + +Both providers require `RUSHDB_DASHBOARD_URL` to be set for redirect URIs. + +--- + +## Rate Limiting + +| Variable | Default | Description | +| ----------------------------- | ------- | ------------------------------ | +| `RATE_LIMITER_REQUESTS_LIMIT` | `10` | Max requests per window per IP | +| `RATE_LIMITER_TTL` | `1000` | Window size in milliseconds | + +--- + +## Pagination + +| Variable | Default | Description | +| --------------------------------- | ------- | ----------------------------------------------------------------- | +| `RUSHDB_PAGINATION_DEFAULT_LIMIT` | `100` | Default page size when `limit` is not specified in a search query | +| `RUSHDB_PAGINATION_MAX_LIMIT` | `1000` | Maximum allowed `limit` value. Requests above this are clamped. | + +--- + +## Advanced + +### MCP OAuth — RS256 JWT Signing + +By default MCP OAuth tokens are signed with the symmetric `RUSHDB_AES_256_ENCRYPTION_KEY`. For production deployments that need JWKS discovery (RS256), set an asymmetric key pair instead. + +| Variable | Default | Description | +| ------------------------------- | ------------------ | ------------------------------------------------------------------------------------ | +| `RUSHDB_JWT_PRIVATE_KEY` | — | PEM-encoded RSA private key (multiline). Prefer the `_BASE64` variant in containers. | +| `RUSHDB_JWT_PUBLIC_KEY` | — | PEM-encoded RSA public key | +| `RUSHDB_JWT_PRIVATE_KEY_BASE64` | — | Base64-encoded PEM private key — easier to pass in env without escaping | +| `RUSHDB_JWT_PUBLIC_KEY_BASE64` | — | Base64-encoded PEM public key | +| `RUSHDB_JWT_KID` | `rushdb-mcp-rs256` | Key ID published in `/.well-known/jwks.json` | + +Generate a key pair: + +```bash +openssl genrsa -out private.pem 2048 +openssl rsa -in private.pem -pubout -out public.pem +# Base64-encode for containers: +base64 -i private.pem | tr -d '\n' # → RUSHDB_JWT_PRIVATE_KEY_BASE64 +base64 -i public.pem | tr -d '\n' # → RUSHDB_JWT_PUBLIC_KEY_BASE64 +``` + +### Captcha + +| Variable | Default | Description | +| --------------------- | ------- | ------------------------------------------------ | +| `SERVICE_CAPTCHA_KEY` | — | Server-side private key for CAPTCHA verification | + +## Core + +| Variable | Default | Description | +| ----------------------- | ------- | ------------------------------------------------------------------------------------------ | +| `RUSHDB_PORT` | `3000` | Port the RushDB API listens on | +| `RUSHDB_LOG_LEVEL` | `info` | Log verbosity: `debug` \| `info` \| `warn` \| `error` | +| `RUSHDB_ENCRYPTION_KEY` | — | **Required.** 32-byte hex key for at-rest encryption. Generate with `openssl rand -hex 32` | +| `RUSHDB_ENCRYPTION_IV` | — | **Required.** 16-byte hex IV. Generate with `openssl rand -hex 16` | + +## Database (SQL) + +RushDB uses a relational database for project metadata and user data. + +| Variable | Default | Description | +| ------------------- | ------------------ | ----------------------------------------------- | +| `SQL_DB_TYPE` | `sqlite` | `sqlite` or `postgres` | +| `SQL_DB_PATH` | `./data/rushdb.db` | Path to SQLite file (when `SQL_DB_TYPE=sqlite`) | +| `POSTGRES_HOST` | `localhost` | PostgreSQL host | +| `POSTGRES_PORT` | `5432` | PostgreSQL port | +| `POSTGRES_USER` | `rushdb` | PostgreSQL user | +| `POSTGRES_PASSWORD` | — | PostgreSQL password | +| `POSTGRES_DB` | `rushdb` | Database name | + +See [PostgreSQL / SQLite](/deploy/infra-database) for setup details. + +## Neo4j (Graph) + +| Variable | Default | Description | +| ---------------- | ----------------------- | ---------------------------- | +| `NEO4J_URL` | `bolt://localhost:7687` | Neo4j Bolt connection URL | +| `NEO4J_USERNAME` | `neo4j` | Neo4j username | +| `NEO4J_PASSWORD` | — | **Required.** Neo4j password | + +See [Neo4j & Aura](/deploy/infra-neo4j) for setup details. + +## Embeddings (AI Search) + +| Variable | Default | Description | +| ----------------------- | ------- | ------------------------------------------- | +| `EMBEDDINGS_PROVIDER` | — | `openai` \| `cohere` \| `custom` | +| `EMBEDDINGS_MODEL` | — | Model name (e.g. `text-embedding-3-small`) | +| `EMBEDDINGS_API_KEY` | — | API key for the embedding provider | +| `EMBEDDINGS_DIMENSIONS` | — | Vector dimensions (must match model output) | + +## Authentication + +| Variable | Default | Description | +| ------------ | ------- | ----------------------------------------------------------------------- | +| `JWT_SECRET` | — | **Required.** Secret for signing JWT tokens. Use `openssl rand -hex 64` | +| `JWT_EXPIRY` | `7d` | Token expiry duration (e.g. `1d`, `7d`, `30d`) | + +See [Authorization](/deploy/get-api-key) for API key management. diff --git a/docs/docs/tutorials/configuring-dashboard.md b/docs/docs/deploy/configuration/get-api-key.md similarity index 88% rename from docs/docs/tutorials/configuring-dashboard.md rename to docs/docs/deploy/configuration/get-api-key.md index 5db5bb18..805bba43 100644 --- a/docs/docs/tutorials/configuring-dashboard.md +++ b/docs/docs/deploy/configuration/get-api-key.md @@ -1,4 +1,5 @@ --- +slug: /deploy/get-api-key title: Get API Key description: Register for RushDB, create a project, and generate your first API token to start building. sidebar_position: 1 @@ -6,6 +7,7 @@ tags: [Getting Started] --- # Get API Key + In this section, we'll walk through the process of registering for RushDB and generating an API token necessary for using the RushDB SDK. This token is essential for authenticating your application's requests to the RushDB backend. ## Step 1: Sign Up for RushDB @@ -18,13 +20,13 @@ Once signed in, you'll be directed to the dashboard. To start working with RushD - Click on the **Create Project** button to set up a new project. You might need to provide some basic information about your project, such as its name. -![Create Project Button](../../static/img/quick-start/create-project-screen.png "Highlighting the 'Create Project' Button") +![Create Project Button](../../../static/img/quick-start/create-project-screen.png "Highlighting the 'Create Project' Button") ## Step 3: Copy an API Key After you create your project, you’ll be taken to its Help page, where an API key will already be available. If needed, you can create additional API tokens on the **API Keys** tab. -![Copy API Key](../../static/img/quick-start/create-token-screen.png "Copy API Key") +![Copy API Key](../../../static/img/quick-start/create-token-screen.png 'Copy API Key') - In the Authorization section, click the automatically generated API token to copy it. This token will be used to authenticate your SDK instances and allow them to interact with your RushDB project. diff --git a/docs/docs/deploy/configuration/index.mdx b/docs/docs/deploy/configuration/index.mdx new file mode 100644 index 00000000..8a667365 --- /dev/null +++ b/docs/docs/deploy/configuration/index.mdx @@ -0,0 +1,56 @@ +--- +slug: /deploy/config-index +title: Configuration +description: Configure environment variables, authorization, and security for your RushDB instance. +sidebar_position: 3 +displayed_sidebar: docs +overview_page: true +tags: [Configuration, Environment Variables, Security, Self-Hosted] +--- + +import { DeployMethodRow, DeployMethodRowList, DeployCrossLinks } from '@site/src/components/DeployMethodCard' + +# Configuration + +Configure RushDB after deployment — environment variables, API key management, and production security. + + + + + + + + diff --git a/docs/docs/deploy/configuration/security.mdx b/docs/docs/deploy/configuration/security.mdx new file mode 100644 index 00000000..aca4ab7c --- /dev/null +++ b/docs/docs/deploy/configuration/security.mdx @@ -0,0 +1,396 @@ +--- +slug: /deploy/config-security +title: Security +description: How RushDB stores data, enforces project isolation, and what to harden in production. +sidebar_position: 3 +displayed_sidebar: docs +tags: [Configuration, Security, Self-Hosted, Production] +--- + +# Security + +## Production Checklist + +- [ ] TLS termination in front of `RUSHDB_PORT` (never expose HTTP to the internet) +- [ ] Neo4j Bolt (`7687`) and Browser (`7474`) ports **not** reachable from the public internet +- [ ] `RUSHDB_AES_256_ENCRYPTION_KEY` is a random 32-character string — not the default +- [ ] `RUSHDB_PASSWORD` changed from `password` +- [ ] `.env` not committed to version control +- [ ] SQL and Neo4j data volumes backed up + +--- + +## Data Storage Architecture + +RushDB uses **two databases in tandem**: + +| Layer | Database | What lives here | +| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------- | +| **Metadata** | SQL (SQLite or PostgreSQL) | Users, workspaces, projects, API tokens, OAuth sessions, workspace invites, project access grants | +| **Graph data** | Neo4j | Records, property nodes, relationships — your actual application data | + +### SQL layer + +The SQL schema is managed by [Drizzle ORM](https://orm.drizzle.team). The main tables are: + +``` +users – login, hashed password, OAuth accounts +workspaces – top-level tenant grouping +workspace_members – (userId, workspaceId, role, since) +projects – (id, workspaceId, name, …) +project_access – (projectId, userId, role, since) +tokens – (id, projectId, level, expiration, value ← AES-256 ciphertext) +oauth_clients – MCP OAuth registered clients +oauth_consents – per-user per-client authorisation grants +``` + +Passwords are stored as bcrypt hashes. Token values are AES-256-CBC ciphertexts (see [API Token Security](#api-token-security) below). + +### Neo4j graph layer + +Every piece of user data in the graph carries two internal system labels/properties that RushDB manages exclusively: + +| Symbol | Value | Purpose | +| ------------------------------- | ------------- | --------------------------------------------------- | +| `__RUSHDB__LABEL__RECORD__` | Node label | Marks every data node as a RushDB record | +| `__RUSHDB__KEY__PROJECT__ID__` | Node property | Scopes the node to a specific project | +| `__RUSHDB__LABEL__PROPERTY__` | Node label | Shared property-type node (name + type + projectId) | +| `__RUSHDB__RELATION__DEFAULT__` | Relationship | User-defined relationship between records | +| `__RUSHDB__RELATION__VALUE__` | Relationship | Edge from record → property-type node | + +A record node looks like this in the graph: + +``` +(:__RUSHDB__LABEL__RECORD__:`YourLabel` { + __RUSHDB__KEY__ID__: "01938abc-…", + __RUSHDB__KEY__PROJECT__ID__: "01937def-…", + // … your fields … +}) +``` + +Neo4j indexes created at startup: + +```cypher +-- Fast record lookups by ID +CREATE INDEX index_record_id FOR (n:__RUSHDB__LABEL__RECORD__) ON (n.__RUSHDB__KEY__ID__) +-- Fast project scoping (used in every query) +CREATE INDEX index_record_projectid FOR (n:__RUSHDB__LABEL__RECORD__) ON (n.__RUSHDB__KEY__PROJECT__ID__) +-- Property name lookups +CREATE INDEX index_property_name FOR (n:__RUSHDB__LABEL__PROPERTY__) ON (n.name) +``` + +--- + +## Project Isolation + +Project isolation is enforced at the **Cypher query level**. Every read and write query pattern includes the projectId as a mandatory node property match: + +```cypher +-- Pattern used in every generated query — the projectId is always a bound parameter +MATCH (record:__RUSHDB__LABEL__RECORD__ { __RUSHDB__KEY__PROJECT__ID__: $projectId }) +``` + +This is analogous to a **mandatory WHERE clause** in a multi-tenant SQL application — similar to how ClickHouse row policies or PostgreSQL Row Level Security add a predicate to every query for a given role: + +```sql +-- SQL equivalent (what RushDB does in Cypher) +SELECT * FROM records WHERE project_id = $projectId +``` + +Because `$projectId` is derived from the validated API token (extracted server-side, never from user input), a caller cannot query another project's records by substituting a different ID — the token value is verified, decrypted to its ID, joined to the `tokens` SQL table, and the `projectId` field on that row is used as the binding. + +**Property-type nodes** are similarly scoped: the uniqueness constraint includes `projectId`: + +```cypher +CREATE CONSTRAINT FOR (p:__RUSHDB__LABEL__PROPERTY__) +REQUIRE (p.name, p.type, p.projectId, p.metadata) IS UNIQUE +``` + +This means two projects can each have a property named `email` of type `string` and they will be distinct nodes that never overlap. + +**What isolation does NOT cover:** All projects on a single RushDB instance share the same Neo4j database. There is no database-level or graph-level partition — isolation is purely by `projectId` property filter. For strongest physical isolation, run separate RushDB instances per tenant with separate Neo4j instances. + +--- + +## API Token Security + +API tokens are UUIDs encrypted with AES-256-CBC before being stored in SQL: + +``` +token ID (UUID v7) + │ + ▼ +AES-256-CBC(key=RUSHDB_AES_256_ENCRYPTION_KEY, iv=random 16 bytes) + │ + ▼ +hex(iv) + base64(ciphertext) ← stored in tokens.value +``` + +The token value presented by the client (`Authorization: Bearer `) is the ciphertext. On every API request: + +1. Decrypt the bearer value with `RUSHDB_AES_256_ENCRYPTION_KEY` to recover the token UUID +2. Look up the UUID in the `tokens` SQL table +3. Verify the token is not expired (`created + expiration_ms > now`) +4. Read `tokens.projectId` and `tokens.level` from the row +5. Inject `projectId` as a bound parameter into all Cypher queries for this request + +Because the UUID is the lookup key and the ciphertext is what travels over the wire, a database dump of the `tokens` table is not sufficient to forge a valid token — you also need `RUSHDB_AES_256_ENCRYPTION_KEY`. + +**Token access levels:** + +| Level | Allowed operations | +| ------- | ---------------------------------- | +| `write` | All CRUD + relationship management | +| `read` | Read-only queries and searches | + +--- + +## Authentication & Access Control + +### Admin account (self-hosted bootstrap) + +When `RUSHDB_SELF_HOSTED=true`, RushDB creates an admin account on first startup using the values of two environment variables: + +| Variable | Default | Purpose | +| ----------------- | ---------- | ------------------------------------ | +| `RUSHDB_LOGIN` | `admin` | Admin account username (email/login) | +| `RUSHDB_PASSWORD` | `password` | Admin account password | + +The bootstrap logic runs once per startup and only creates the user if they **do not yet exist**: + +``` +RUSHDB_SELF_HOSTED=true + RUSHDB_LOGIN + RUSHDB_PASSWORD + │ + ▼ + User already exists? + ├─ Yes → skip (env vars are ignored, stored bcrypt hash is unchanged) + └─ No → create user, bcrypt hash the password (10 rounds), seed default workspace +``` + +:::warning Change the defaults before first start +`RUSHDB_LOGIN` and `RUSHDB_PASSWORD` are only used to **create** the account. After the account exists, changing these environment variables has no effect on the stored password. Set them to production values before the first boot — not after. +::: + +**To change the password after first boot**, use the built-in CLI: + +```bash +# Docker +docker exec -it rushdb node dist/main update-password admin new-secure-password + +# docker compose +docker compose exec rushdb node dist/main update-password admin new-secure-password + +# From source +cd platform/core && node dist/main update-password admin new-secure-password +``` + +**To create additional admin users**: + +```bash +docker exec -it rushdb node dist/main create-user alice@example.com my-password +``` + +CLI commands are only available when `RUSHDB_SELF_HOSTED=true`. + +--- + +### Restricting sign-ups + +By default, any email can register an account on the dashboard (relevant when OAuth providers are configured). To restrict this to a specific allow-list: + +```env +RUSHDB_ALLOWED_LOGINS=["alice@example.com","bob@example.com"] +``` + +When `RUSHDB_ALLOWED_LOGINS` is non-empty, attempts to create accounts or accept workspace invitations from unlisted emails are rejected with `400 Bad Request`. + +--- + +### Sessions (dashboard / human users) + +Sessions are JWT-signed using `RUSHDB_AES_256_ENCRYPTION_KEY` (HS256 by default; RS256 when `RUSHDB_JWT_PRIVATE_KEY` is configured). See [Environment Variables — MCP OAuth](/deploy/config-env-variables#mcp-oauth--rs256-jwt-signing) for the RS256 key setup. + +Access is checked at two layers on every dashboard request: + +1. **Workspace membership** — verified via `workspace_members` SQL table. Required for any dashboard action. +2. **Project access** — verified via `project_access` SQL table. Users can be Owner or Editor per project. + +Roles: + +| Role | Scope | Capabilities | +| -------- | --------- | -------------------------------------------------- | +| `owner` | Project | Full access including delete and access management | +| `editor` | Project | Read/write data, manage tokens | +| `admin` | Workspace | Manage workspace members and all projects | + +--- + +### API tokens (programmatic access) + +All API calls authenticate with `Authorization: Bearer `. Tokens are created per-project from the dashboard and are scoped to exactly one project — there are no cross-project tokens. Token access levels: + +| Level | Allowed operations | +| ------- | ---------------------------------- | +| `write` | All CRUD + relationship management | +| `read` | Read-only queries and searches | + +See [API Token Security](#api-token-security) above for the full encryption pipeline. + +--- + +## TLS / HTTPS + +Never expose port `3000` without TLS in production. + +**Caddy** (automatic TLS via Let's Encrypt): + +```caddyfile +api.yourdomain.com { + reverse_proxy localhost:3000 +} +``` + +**nginx**: + +```nginx +server { + listen 443 ssl; + server_name api.yourdomain.com; + ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +--- + +## Network Isolation + +- `NEO4J_URL` bolt port (`7687`) — **internal only**. RushDB talks to Neo4j directly; this port must never be reachable from the internet. +- Neo4j Browser (`7474`, `7473`) — disable or firewall in production. Only needed for ad-hoc admin queries. +- PostgreSQL (`5432`) — internal only. +- SSH (`22`) — restrict to your IP range. + +For Docker Compose deployments, do not publish Neo4j or PostgreSQL ports unless you need them for local dev: + +```yaml +# ✅ internal only — do NOT add ports: mapping in production +neo4j: + image: neo4j:2026.01.4 + # ports: – omit this section +``` + +--- + +## Secrets Management + +Generate strong random values: + +```bash +# Encryption key +openssl rand -hex 32 + +# Or a 32-character alphanumeric string +LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*' ` header. See [Authorization](/deploy/get-api-key) for key management. diff --git a/docs/docs/tutorials/connect-aura-instance.mdx b/docs/docs/deploy/guides/connect-aura.mdx similarity index 81% rename from docs/docs/tutorials/connect-aura-instance.mdx rename to docs/docs/deploy/guides/connect-aura.mdx index a09909fb..535c0b1b 100644 --- a/docs/docs/tutorials/connect-aura-instance.mdx +++ b/docs/docs/deploy/guides/connect-aura.mdx @@ -1,12 +1,13 @@ --- +slug: /deploy/connect-aura sidebar_position: 34 -title: "Connecting a Neo4j Aura Instance (BYOC)" +title: 'Connecting a Neo4j Aura Instance (BYOC)' description: Use your own Neo4j or Aura database as the RushDB data store — keep data in your cloud account while using the full RushDB API. tags: [Deployment, BYOC, Neo4j, Aura] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Connecting a Neo4j Aura Instance (BYOC) @@ -26,6 +27,7 @@ graph LR ``` RushDB stores: + - **In your Neo4j/Aura** — all record nodes, relationship edges, and vector embeddings - **In RushDB's Postgres** — project config, API keys, embedding index metadata, and billing records only @@ -174,26 +176,26 @@ environment: NEO4J_PASSWORD: your-aura-password ``` -See [Self-Hosting RushDB](./deployment.mdx) for the complete Docker Compose setup. +See [Self-Hosting RushDB](/deploy/docker-remote) for the complete Docker Compose setup. --- ## What changes with BYOC vs managed -| | Managed (default) | BYOC | -|---|---|---| -| Data location | RushDB-managed Neo4j | Your Aura / Neo4j | -| Raw Cypher access | No | Yes — `POST /query/raw` | -| Billing | KU-based | KU-based (same) | -| Wipe / restore | Via RushDB dashboard | Full Neo4j backup tools available | -| SLA for the graph | RushDB SLA | Your Neo4j Aura SLA | +| | Managed (default) | BYOC | +| ----------------- | -------------------- | --------------------------------- | +| Data location | RushDB-managed Neo4j | Your Aura / Neo4j | +| Raw Cypher access | No | Yes — `POST /query/raw` | +| Billing | KU-based | KU-based (same) | +| Wipe / restore | Via RushDB dashboard | Full Neo4j backup tools available | +| SLA for the graph | RushDB SLA | Your Neo4j Aura SLA | -See [BYOC vs Managed vs Self-Hosted](./byoc-vs-managed.mdx) for a full comparison. +See [BYOC vs Managed vs Self-Hosted](/tutorials/byoc-vs-managed) for a full comparison. --- ## Next steps -- [BYOC vs Managed vs Self-Hosted](./byoc-vs-managed.mdx) — choose the right topology -- [Self-Hosting RushDB](./deployment.mdx) — run both RushDB and the graph on your own infra -- [Project Setup After Deployment](./self-hosted-project-setup.mdx) — configure embedding models and team access per project +- [BYOC vs Managed vs Self-Hosted](/tutorials/byoc-vs-managed) — choose the right topology +- [Self-Hosting RushDB](/deploy/docker-remote) — run both RushDB and the graph on your own infra +- [Project Setup After Deployment](/deploy/self-hosted-project-setup) — configure embedding models and team access per project diff --git a/docs/docs/tutorials/mcp-operator-quickstart.mdx b/docs/docs/deploy/guides/mcp-operator-quickstart.mdx similarity index 91% rename from docs/docs/tutorials/mcp-operator-quickstart.mdx rename to docs/docs/deploy/guides/mcp-operator-quickstart.mdx index 6cc7fc32..09dfe2e5 100644 --- a/docs/docs/tutorials/mcp-operator-quickstart.mdx +++ b/docs/docs/deploy/guides/mcp-operator-quickstart.mdx @@ -1,13 +1,14 @@ --- +slug: /deploy/mcp-operator-quickstart sidebar_position: 13 -title: "MCP Quickstart for Real Operators: Claude, Cursor, and VS Code" +title: 'MCP Quickstart for Real Operators: Claude, Cursor, and VS Code' description: Go beyond installation — learn the operator workflow for grounded, hallucination-resistant agent queries with the RushDB MCP server. tags: [MCP, Agents, Tooling, Claude, Cursor] --- # MCP Quickstart for Real Operators: Claude, Cursor, and VS Code -The [MCP quickstart](../mcp-server/quickstart.mdx) covers installation in three lines. This tutorial covers what happens after the config file is saved. +The [MCP quickstart](/connect/mcp) covers installation in three lines. This tutorial covers what happens after the config file is saved. Most MCP tutorials stop at "it works." Real operators need to know: @@ -21,7 +22,7 @@ Most MCP tutorials stop at "it works." Real operators need to know: ## Prerequisites - RushDB project with at least one label and a few records -- RushDB MCP server installed and running (see [quickstart](../mcp-server/quickstart.mdx)) +- RushDB MCP server installed and running (see [quickstart](/connect/mcp)) - Claude Desktop, Cursor, or VS Code with Copilot agent mode --- @@ -263,6 +264,6 @@ For write operations (`createRecord`, `attachRelation`, `bulkDeleteRecords`), al ## Next steps -- [MCP tools reference](../mcp-server/tools.mdx) — full tool list with input schemas -- [Agent-Safe Query Planning with Ontology First](./agent-safe-query-planning.mdx) — building repeatable agent patterns with failure recovery -- [Hybrid Retrieval: Structured Filters Plus Semantic Search](./hybrid-retrieval.mdx) — combining semantic search with graph traversal +- [MCP tools reference](/connect/mcp) — full tool list with input schemas +- [Agent-Safe Query Planning with Ontology First](/tutorials/agent-safe-query-planning) — building repeatable agent patterns with failure recovery +- [Hybrid Retrieval: Structured Filters Plus Semantic Search](/tutorials/hybrid-retrieval) — combining semantic search with graph traversal diff --git a/docs/docs/tutorials/self-hosted-project-setup.mdx b/docs/docs/deploy/guides/self-hosted-project-setup.mdx similarity index 87% rename from docs/docs/tutorials/self-hosted-project-setup.mdx rename to docs/docs/deploy/guides/self-hosted-project-setup.mdx index 9334202e..8674651e 100644 --- a/docs/docs/tutorials/self-hosted-project-setup.mdx +++ b/docs/docs/deploy/guides/self-hosted-project-setup.mdx @@ -1,16 +1,17 @@ --- +slug: /deploy/self-hosted-project-setup sidebar_position: 37 -title: "Project Setup After Deployment" +title: 'Project Setup After Deployment' description: Create projects, configure per-project embedding models, invite team members, and verify SDK connectivity on a self-hosted RushDB instance. tags: [Deployment, Self-Hosted, Configuration] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Project Setup After Deployment -Once RushDB is running (see [Self-Hosting RushDB](./deployment.mdx)), the next steps are: +Once RushDB is running (see [Self-Hosting RushDB](/deploy/docker-remote)), the next steps are: 1. Create a project 2. Generate an API key @@ -32,7 +33,7 @@ Open `http://your-host:3000` (or whatever host and port you configured). Sign in 2. Give the project a name (e.g. `production`, `staging`, `my-app`). 3. Choose the data store: - **Managed (default)** — RushDB uses the Neo4j instance configured in server env vars - - **My own Neo4j / Aura** — Toggle on and enter your connection URI and credentials. See [Connecting an Aura Instance](./connect-aura-instance.mdx) for the full walkthrough. + - **My own Neo4j / Aura** — Toggle on and enter your connection URI and credentials. See [Connecting an Aura Instance](/deploy/connect-aura) for the full walkthrough. 4. Click **Create**. Each project is isolated: records, relationships, and embedding indexes in one project are never visible from another. @@ -149,6 +150,7 @@ Per-project embedding configuration is only relevant for **managed** embedding i ## Step 7: Invite team members (cloud) On RushDB Cloud: + 1. Open **Workspace** → **Team**. 2. Click **Invite Member** and enter the email address. 3. Assign a role: **Admin** or **Member**. @@ -159,6 +161,6 @@ On self-hosted RushDB the team member feature is available depending on your pla ## Next steps -- [Self-Hosting RushDB](./deployment.mdx) — deploy with Docker Compose -- [Connecting an Aura Instance](./connect-aura-instance.mdx) — use your own Neo4j as the data store -- [BYOC vs Managed vs Self-Hosted](./byoc-vs-managed.mdx) — compare deployment topologies +- [Self-Hosting RushDB](/deploy/docker-remote) — deploy with Docker Compose +- [Connecting an Aura Instance](/deploy/connect-aura) — use your own Neo4j as the data store +- [BYOC vs Managed vs Self-Hosted](/tutorials/byoc-vs-managed) — compare deployment topologies diff --git a/docs/docs/deploy/infrastructure/index.mdx b/docs/docs/deploy/infrastructure/index.mdx new file mode 100644 index 00000000..f62d2a54 --- /dev/null +++ b/docs/docs/deploy/infrastructure/index.mdx @@ -0,0 +1,59 @@ +--- +slug: /deploy/infra-index +title: Infrastructure +description: Configure RushDB's data layer — the graph backend (Neo4j or Aura) and the relational database (PostgreSQL or SQLite). +sidebar_position: 10 +displayed_sidebar: docs +overview_page: true +tags: [Infrastructure, Neo4j, PostgreSQL, SQLite] +--- + +import { DeployMethodRow, DeployMethodRowList, DeployCrossLinks } from '@site/src/components/DeployMethodCard' + +# Infrastructure + +RushDB is made up of three layers: the **NestJS platform** (the API + dashboard process), **Neo4j** for the graph layer, and a **relational database** for metadata. Configure all three before going to production. + + + + + + + + diff --git a/docs/docs/deploy/infrastructure/neo4j-and-aura.mdx b/docs/docs/deploy/infrastructure/neo4j-and-aura.mdx new file mode 100644 index 00000000..fae26dce --- /dev/null +++ b/docs/docs/deploy/infrastructure/neo4j-and-aura.mdx @@ -0,0 +1,158 @@ +--- +slug: /deploy/infra-neo4j +title: Neo4j & Aura +description: Configure RushDB's graph backend — bundled Neo4j via Docker Compose, an existing Neo4j instance, or Neo4j Aura (BYOC). +sidebar_position: 1 +displayed_sidebar: docs +tags: [Infrastructure, Neo4j, Aura, BYOC, Self-Hosted] +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# Neo4j & Aura + +RushDB uses Neo4j as its graph backend. There are three ways to provide a Neo4j instance: + +| Option | When to use | +| ---------------------------- | -------------------------------------------------------------------- | +| **Bundled** (Docker Compose) | Quickest path — Neo4j runs in the same Compose stack | +| **Existing instance** | You already run Neo4j on your infrastructure | +| **Neo4j Aura (BYOC)** | Keep graph data in your own cloud account; use RushDB API layer only | + +## Requirements + +| Requirement | Details | +| ----------------- | --------------------------------------------------------------------------------------------- | +| **Neo4j version** | `2026.01.4` (tested and bundled). Any `2026.x` release is compatible. | +| **APOC Core** | **Required.** Version must match Neo4j — use `apoc-2026.01.4-core.jar` for the bundled image. | +| **APOC Extended** | Not required. | + +:::danger APOC Core is not optional +Every core RushDB operation — creating records, updating properties, relationship management, batch imports, and schema mutations — relies on APOC Core procedures (`apoc.create.*`, `apoc.periodic.iterate`, `apoc.map.*`, `apoc.convert.*`, `apoc.do.when`). A Neo4j instance without APOC Core will fail immediately on first use. +::: + +### APOC Core procedures used + +| Module | Procedures | +| --------------- | --------------------------------------------------------------- | +| `apoc.create` | `addLabels`, `removeLabels`, `removeProperties`, `relationship` | +| `apoc.periodic` | `iterate` (batch upserts and deletes) | +| `apoc.do` | `when` (conditional record creation in upserts) | +| `apoc.map` | `fromPairs`, `merge`, `removeKey`, `setEntry` | +| `apoc.convert` | `toJson`, `fromJsonMap`, `toList` | + +## Option A — Bundled via Docker Compose + +The standard `docker-compose.yml` ships with Neo4j `2026.01.4` and the matching `apoc-2026.01.4-core.jar` pre-installed. No extra steps required — just set the password in `.env`: + +```env +NEO4J_PASSWORD=your-strong-password +``` + +The APOC Core jar is mounted from `./neo4j-plugins/` into `/var/lib/neo4j/plugins` inside the container. Alternatively, let Neo4j download it at startup by uncommenting the plugins line in `docker-compose.yml`: + +```yaml +environment: + - NEO4J_PLUGINS=["apoc"] # fetches APOC from the internet on first start +``` + +## Option B — Existing Neo4j Instance + +Point RushDB at any running Neo4j `2026.x` instance: + +```env +NEO4J_URL=bolt://your-neo4j-host:7687 +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=your-password +``` + +**APOC Core installation on an existing instance:** + + + + +Download `apoc-2026.01.4-core.jar` from the [APOC releases](https://github.com/neo4j/apoc/releases) page. The filename must start with `apoc-` and the version must match your Neo4j version exactly. + +```bash +# Copy into the Neo4j plugins directory +cp apoc-2026.01.4-core.jar /var/lib/neo4j/plugins/ + +# Restart Neo4j +systemctl restart neo4j +``` + +Verify APOC is loaded: + +```bash +cypher-shell -u neo4j -p yourpassword \ + "CALL apoc.help('create') YIELD name RETURN name LIMIT 1" +``` + + + + +Neo4j's official Docker image can fetch APOC at startup: + +```yaml +environment: + - NEO4J_PLUGINS=["apoc"] +``` + +This requires internet access from the Neo4j container on first start. The version downloaded matches the Neo4j image version automatically. + + + + +:::warning Version matching +The APOC jar version must match the Neo4j server version. Mismatched versions cause startup errors. If you upgrade Neo4j, replace the APOC jar with the matching release. +::: + +## Option C — Neo4j Aura (BYOC) + +Bring Your Own Cloud: keep your graph data in a Neo4j Aura account while RushDB provides the API layer. + +**Why BYOC?** + +- Graph data never leaves your cloud account +- Use Neo4j's managed backups, monitoring, and scaling +- AuraDB Free tier (1 GB) is sufficient for development + +:::info APOC on Aura +Neo4j AuraDB Professional and Enterprise tiers include APOC Core. APOC is **not available** on AuraDB Free. For production BYOC, use AuraDB Professional or higher. +::: + +**Setup:** + +1. Create an instance at [console.neo4j.io](https://console.neo4j.io) +2. Copy the connection URI, username, and password from the Aura console +3. In your RushDB `.env`: + +```env +NEO4J_URL=neo4j+s://xxxxxxxx.databases.neo4j.io +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=your-aura-password +``` + +4. When creating a project in the RushDB dashboard, select **BYOC** and enter the same credentials + +:::info +BYOC projects unlock raw Cypher access via `POST /api/v1/db/{projectId}/query/raw` — useful for advanced graph traversals outside the standard RushDB query model. +::: + +## Verify Connection + +After starting RushDB, check that Neo4j and APOC are reachable: + +```bash +curl http://localhost:3000/health +# → {"status":"ok","neo4j":"connected"} +``` + +To confirm APOC is available from inside Neo4j: + +```bash +cypher-shell -u neo4j -p yourpassword \ + "RETURN apoc.version() AS apocVersion" +# → apocVersion: "2026.01.4" +``` diff --git a/docs/docs/deploy/infrastructure/postgresql-sqlite.mdx b/docs/docs/deploy/infrastructure/postgresql-sqlite.mdx new file mode 100644 index 00000000..29c9318f --- /dev/null +++ b/docs/docs/deploy/infrastructure/postgresql-sqlite.mdx @@ -0,0 +1,83 @@ +--- +slug: /deploy/infra-database +title: PostgreSQL / SQLite +description: Configure the relational database used by RushDB for project metadata, users, and API keys. +sidebar_position: 2 +displayed_sidebar: docs +tags: [Infrastructure, PostgreSQL, SQLite, Self-Hosted] +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# PostgreSQL / SQLite + +RushDB uses a relational database (separate from Neo4j) to store project metadata, user accounts, and API keys. + +## Choosing a Backend + +| | SQLite | PostgreSQL | +| --------------- | ------------------------------------- | ------------------------------------ | +| **Setup** | Zero config | Requires a running Postgres instance | +| **Best for** | Single-node, low traffic, dev/staging | Production, HA, multi-instance | +| **Persistence** | File on disk | External service | +| **Scale** | Single process | Horizontal scaling | + +Use **SQLite** to get started quickly. Migrate to **PostgreSQL** before going to production if you need concurrent writes or plan to run multiple RushDB instances. + +## SQLite Setup + +Set in `.env`: + +```env +SQL_DB_TYPE=sqlite +SQL_DB_PATH=./data/rushdb.db +``` + +Ensure the `data/` directory is persisted via a Docker volume: + +```yaml +# docker-compose.yml (excerpt) +volumes: + - ./data:/app/data +``` + +## PostgreSQL Setup + + + + +The full Compose stack ships with a PostgreSQL 16 service. Set credentials in `.env`: + +```env +SQL_DB_TYPE=postgres +SQL_DB_URL=postgresql://rushdb:your-strong-password@postgres:5432/rushdb +``` + + + + +Point RushDB at an existing PostgreSQL 14+ instance: + +```env +SQL_DB_TYPE=postgres +SQL_DB_URL=postgresql://rushdb:your-strong-password@your-pg-host.example.com:5432/rushdb +``` + +Create the database and user before starting RushDB: + +```sql +CREATE USER rushdb WITH PASSWORD 'your-strong-password'; +CREATE DATABASE rushdb OWNER rushdb; +``` + + + + +## Backups + +Always back up the relational database alongside Neo4j. For SQLite, copy the `.db` file. For PostgreSQL, use `pg_dump`: + +```bash +pg_dump -h localhost -U rushdb rushdb > rushdb_backup_$(date +%Y%m%d).sql +``` diff --git a/docs/docs/deploy/infrastructure/rushdb-platform.mdx b/docs/docs/deploy/infrastructure/rushdb-platform.mdx new file mode 100644 index 00000000..00fb7c74 --- /dev/null +++ b/docs/docs/deploy/infrastructure/rushdb-platform.mdx @@ -0,0 +1,162 @@ +--- +slug: /deploy/infra-platform +title: RushDB Platform +description: How the RushDB NestJS application process works — ports, startup dependencies, static serving, health check, CLI, and resource requirements. +sidebar_position: 1 +displayed_sidebar: docs +tags: [Infrastructure, NestJS, Node.js, Self-Hosted] +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# RushDB Platform + +RushDB is a single **NestJS** application built on the **Fastify** adapter. It serves both the REST API and (optionally) the built-in dashboard as static files from the same process. + +## Runtime overview + +| Attribute | Value | +| ----------------- | -------------------------------------------- | +| Framework | NestJS + Fastify | +| Node.js | 18 – 22 | +| Default port | `3000` (all interfaces — `0.0.0.0`) | +| API prefix | `/api/v1` | +| Swagger UI | `/api` | +| Body limit | 2 GB (for large batch imports) | +| Health endpoint | `GET /` → `"RushDB is running"` | +| Settings endpoint | `GET /settings` → OAuth flags, feature flags | + +--- + +## Key environment variables + +| Variable | Required | Default | Description | +| ------------------------------- | :------: | ---------- | --------------------------------------------------------------------------------------------------------- | +| `RUSHDB_SELF_HOSTED` | ✅ | — | Must be `true`. Enables self-hosted mode, disables billing, seeds the admin account. | +| `RUSHDB_PORT` | | `3000` | Port the API listens on. | +| `RUSHDB_LOGIN` | ✅ | `admin` | Admin account login created on first startup. | +| `RUSHDB_PASSWORD` | ✅ | `password` | Admin password — used **only** if the account does not yet exist. | +| `RUSHDB_AES_256_ENCRYPTION_KEY` | ✅ | — | Exactly 32 characters. AES-256 key for token encryption and JWT signing. Generate: `openssl rand -hex 32` | +| `RUSHDB_SERVE_STATIC` | | `false` | Set to `true` to serve the dashboard from the same process (see below). | +| `RUSHDB_DASHBOARD_URL` | | — | Public URL of the dashboard. Used in OAuth redirects and email links. | +| `RUSHDB_PUBLIC_URL` | | — | Public URL of the API. Used as the OAuth issuer (`RUSHDB_OAUTH_ISSUER`). | + +See [Environment Variables](/deploy/config-env-variables) for the full reference. + +--- + +## Startup dependencies + +RushDB will crash on startup if it cannot reach Neo4j or the SQL database. The startup order must be: + +``` +1. Neo4j (bolt :7687 must accept connections) +2. PostgreSQL / SQLite file accessible +3. RushDB platform process +``` + +In the Docker Compose setup this is enforced with `depends_on` + `condition: service_healthy` on both Neo4j and PostgreSQL. When running from source, start the databases first. + +:::tip Built-in wait-for-Neo4j +The `DatabaseModule` retries the Neo4j HTTP health endpoint (`http://neo4j:7474`) up to 15 times with a 5-second interval before raising an error. This provides ~75 seconds of tolerance for slow Neo4j startup. +::: + +--- + +## Dashboard serving + +RushDB can serve the built-in dashboard from the same process or delegate it to a CDN / separate origin. + + + + +Set `RUSHDB_SERVE_STATIC=true`. The `public/` directory (pre-built dashboard assets) is served at `/*`, excluding `/api*`. + +```env +RUSHDB_SERVE_STATIC=true +RUSHDB_DASHBOARD_URL=https://rushdb.example.com +``` + +Requests to `/` return the dashboard HTML. Requests to `/api/v1/*` are handled by the API router. + +:::note +When `RUSHDB_SERVE_STATIC=true`, the `GET /` health endpoint is replaced by the dashboard index page. Use `GET /settings` for liveness probes in this mode. +::: + + + + +Set `RUSHDB_SERVE_STATIC=false` (default). Host the dashboard separately (CDN, Vercel, Nginx static, etc.) and point it at the API URL. + +```env +RUSHDB_SERVE_STATIC=false +RUSHDB_DASHBOARD_URL=https://app.rushdb.example.com +``` + +`GET /` returns `"RushDB is running"` — useful as a lightweight health check. + + + + +--- + +## CORS + +CORS is enabled by default and accepts requests from any origin (`origin: true`). Custom headers allowed in requests: + +``` +Authorization +X-Workspace-Id +X-Project-Id +X-Transaction-Id +Content-Disposition +Content-Type +``` + +If you need to restrict origins, place a reverse proxy (nginx, Caddy) in front and enforce `Origin` checking there — the application-level CORS is intentionally permissive for self-hosted flexibility. + +--- + +## Swagger / OpenAPI + +The OpenAPI spec is auto-generated and served at: + +``` +http(s)://your-host:3000/api +``` + +All API endpoints require a `Bearer` token. The Swagger UI has a built-in "Authorize" button to paste a project token. + +--- + +## CLI commands + +RushDB ships with a console CLI (requires `RUSHDB_SELF_HOSTED=true`). Run from the container or from `platform/core/dist/`: + +```bash +# Change a user's password +docker exec -it rushdb node dist/main update-password + +# Create a new user +docker exec -it rushdb node dist/main create-user + +# Restore a project from a backup dump +docker exec -it rushdb node dist/main db-restore +``` + +For `docker compose`, replace `docker exec -it rushdb` with `docker compose exec rushdb`. + +--- + +## Resource requirements + +These are baseline figures for a lightly loaded self-hosted instance: + +| Resource | Minimum | Recommended | +| -------- | --------- | -------------------------- | +| CPU | 0.25 vCPU | 1 vCPU | +| Memory | 256 MB | 512 MB – 1 GB | +| Disk | — | Depends on SQLite PVC size | + +Neo4j typically dominates resource consumption. See [Neo4j & Aura](/deploy/infra-neo4j) for its requirements. diff --git a/docs/docs/deploy/local-hosting/docker.mdx b/docs/docs/deploy/local-hosting/docker.mdx new file mode 100644 index 00000000..2a5f1950 --- /dev/null +++ b/docs/docs/deploy/local-hosting/docker.mdx @@ -0,0 +1,186 @@ +--- +slug: /deploy/local-docker +title: Docker +description: Run RushDB locally with Docker Compose (full stack) or a single docker run command against an existing Neo4j instance. +sidebar_position: 1 +displayed_sidebar: docs +tags: [Getting Started, Deployment, Docker] +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# Docker + +Run RushDB on your machine in minutes. Option A is self-contained — no external accounts required. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) 24.0+ +- [Docker Compose](https://docs.docker.com/compose/install/) 2.20+ (included in Docker Desktop) +- **4 GB RAM** available to Docker (Neo4j needs at least 2 GB) + +--- + + + + +## Full Stack with Docker Compose + +RushDB + Neo4j in one Compose file. Copy, adjust the passwords, run. + +```yaml +# docker-compose.yml +version: '3.8' +services: + rushdb: + image: rushdb/platform + container_name: rushdb + depends_on: + neo4j: + condition: service_healthy + ports: + - '3000:3000' + environment: + # ── Required ─────────────────────────────────────────────────────────── + RUSHDB_SELF_HOSTED: 'true' + RUSHDB_LOGIN: admin + RUSHDB_PASSWORD: password # change before exposing to a network + RUSHDB_AES_256_ENCRYPTION_KEY: '32SymbolStringForTokenEncryption' # generate: openssl rand -hex 32 + # ── Neo4j connection ─────────────────────────────────────────────────── + NEO4J_URL: bolt://neo4j:7687 + NEO4J_USERNAME: neo4j + NEO4J_PASSWORD: password # must match NEO4J_AUTH below + # ── Metadata DB (zero-config SQLite default) ─────────────────────────── + SQL_DB_TYPE: sqlite + + neo4j: + image: neo4j:2026.01.4 + healthcheck: + test: ['CMD-SHELL', 'wget --no-verbose --tries=1 --spider localhost:7474 || exit 1'] + interval: 5s + retries: 30 + start_period: 10s + ports: + - '7474:7474' # Neo4j Browser (dev only — block in production) + - '7687:7687' # Bolt (keep internal in production) + environment: + NEO4J_ACCEPT_LICENSE_AGREEMENT: 'yes' + NEO4J_AUTH: neo4j/password # must match NEO4J_PASSWORD above + NEO4J_PLUGINS: '["apoc"]' + volumes: + - neo4j-data:/data + - neo4j-plugins:/var/lib/neo4j/plugins + - neo4j-logs:/logs + +volumes: + neo4j-data: + neo4j-plugins: + neo4j-logs: +``` + +```bash +docker compose up -d +``` + +Open [http://localhost:3000](http://localhost:3000) — sign in with `admin` / `password`. + +:::warning +`RUSHDB_AES_256_ENCRYPTION_KEY` must be **exactly 32 characters**. Wrong length → container crashes on startup. Do not change this value after first boot without migrating encrypted data. + +Generate a secure value: + +```bash +openssl rand -hex 32 +``` + +::: + + + + +## Connect to Existing Neo4j + +Already have Neo4j running locally or on Aura? + +```bash +docker run -d \ + --name rushdb \ + -p 3000:3000 \ + -e RUSHDB_SELF_HOSTED=true \ + -e RUSHDB_LOGIN=admin \ + -e RUSHDB_PASSWORD=password \ + -e 'RUSHDB_AES_256_ENCRYPTION_KEY=32SymbolStringForTokenEncryption' \ + -e NEO4J_URL='bolt://your-neo4j-host:7687' \ + -e NEO4J_USERNAME=neo4j \ + -e NEO4J_PASSWORD=your-neo4j-password \ + -e SQL_DB_TYPE=sqlite \ + rushdb/platform +``` + +For Neo4j Aura use `neo4j+s://` in `NEO4J_URL`. + +:::info Neo4j requirements +The Neo4j instance must have **APOC Core** installed (version must match Neo4j). See [Neo4j & Aura](/deploy/infra-neo4j) for details. +::: + + + + +--- + +## Environment Variables + +| Variable | Required | Default | Description | +| ------------------------------- | :------------: | ------------- | ------------------------------------------------------- | +| `RUSHDB_SELF_HOSTED` | ✅ | — | Must be `true` for self-hosted mode | +| `RUSHDB_LOGIN` | ✅ | — | Admin username | +| `RUSHDB_PASSWORD` | ✅ | — | Admin password | +| `RUSHDB_AES_256_ENCRYPTION_KEY` | ✅ | — | **Exactly 32 characters.** Encrypts API tokens at rest | +| `NEO4J_URL` | ✅ | — | `bolt://neo4j:7687` (Compose) or `neo4j+s://...` (Aura) | +| `NEO4J_USERNAME` | ✅ | `neo4j` | Neo4j username | +| `NEO4J_PASSWORD` | ✅ | — | Must match `NEO4J_AUTH` in the Neo4j container | +| `SQL_DB_TYPE` | — | `sqlite` | Metadata store: `sqlite` or `postgres` | +| `SQL_DB_PATH` | — | `./rushdb.db` | SQLite file path | +| `SQL_DB_URL` | ✅ if postgres | — | `postgresql://user:pass@host:5432/rushdb` | +| `RUSHDB_PORT` | — | `3000` | Port RushDB listens on | + +See [Environment Variables](/deploy/config-env-variables) for the full reference. + +--- + +## Verify + +```bash +docker compose ps +# Both services should show "healthy" / "running" + +curl http://localhost:3000 +# Returns the RushDB dashboard HTML +``` + +--- + +## CLI — Manage Users + +```bash +# Create an additional user +docker exec rushdb rushdb create-user user@example.com newpassword + +# Reset a password +docker exec rushdb rushdb update-password user@example.com newpassword +``` + +--- + +## Troubleshooting + +**Container exits immediately** — run `docker logs rushdb`. Common causes: + +- `RUSHDB_AES_256_ENCRYPTION_KEY` is not exactly 32 characters +- `NEO4J_PASSWORD` doesn't match `NEO4J_AUTH` in the Neo4j container +- Missing a required env var + +**`Connection refused` to Neo4j** — Neo4j takes ~30 s to initialize. The `depends_on: condition: service_healthy` in the Compose file waits automatically. Watch with `docker logs -f neo4j` until you see `Remote interface available`. + +**Container killed / OOM** — Neo4j needs ≥ 2 GB heap. In Docker Desktop go to Settings → Resources and raise memory to at least 4 GB. diff --git a/docs/docs/deploy/local-hosting/from-source.mdx b/docs/docs/deploy/local-hosting/from-source.mdx new file mode 100644 index 00000000..662a3321 --- /dev/null +++ b/docs/docs/deploy/local-hosting/from-source.mdx @@ -0,0 +1,130 @@ +--- +slug: /deploy/local-source +title: From Source +description: Clone the RushDB repository and run the platform locally with pnpm dev — Neo4j and PostgreSQL start in Docker while the NestJS API and dashboard run as local Node.js processes. +sidebar_position: 2 +displayed_sidebar: docs +tags: [Getting Started, Deployment, Development] +--- + +# From Source + +Clone the repo and run everything locally. `pnpm dev` from the repo root starts Neo4j and PostgreSQL in Docker and the NestJS API + Vite dashboard as local Node.js processes — no manual database setup required. + +``` +pnpm dev starts: + ● Neo4j 2026.01.4 — Docker container (bolt :7687, browser :7474) + ● PostgreSQL 16 — Docker container (port :5432) + ● NestJS API — Node.js process (port :3000, watch mode) + ● Vite dashboard — Node.js process (port :3005) +``` + +Useful for contributors, debugging, or running unreleased features before they ship in a Docker image. + +## Prerequisites + +| Tool | Minimum version | Install | +| ------- | :-------------: | -------------------------------- | +| Node.js | 20 | [nodejs.org](https://nodejs.org) | +| pnpm | 9 | `npm i -g pnpm` | +| Docker | 24+ | [docker.com](https://docker.com) | + +Docker is required because `pnpm dev` spins up Neo4j and PostgreSQL via Docker Compose. + +--- + +## 1. Clone the repository + +```bash +git clone https://github.com/rush-db/rushdb.git +cd rushdb +``` + +--- + +## 2. Configure environment + +```bash +cp platform/core/.env.example platform/core/.env +``` + +Open `platform/core/.env` and set at minimum: + +```env +RUSHDB_SELF_HOSTED=true +RUSHDB_LOGIN=admin +RUSHDB_PASSWORD=password + +# Must be exactly 32 characters — generate: openssl rand -hex 32 +RUSHDB_AES_256_ENCRYPTION_KEY=32SymbolStringForTokenEncryption + +# These are auto-wired from Docker Compose — change only if using an external Neo4j +NEO4J_URL=bolt://localhost:7687 +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=password + +# PostgreSQL credentials must match the Compose defaults (or override both) +SQL_DB_TYPE=postgres +SQL_DB_URL=postgresql://rushdb:password@localhost:5432/rushdb +``` + +--- + +## 3. Install dependencies + +```bash +pnpm install +``` + +--- + +## 4. Start dev + +```bash +pnpm dev +``` + +This is equivalent to running concurrently: + +| Process | Command | What it does | +| -------------------- | ---------------------------------- | ------------------------------------------------------- | +| `neo4j` + `postgres` | `docker compose up` | Starts both databases in containers | +| NestJS API | `nest start --watch` | Compiles and runs the backend; restarts on file changes | +| Vite dashboard | `pnpm dev` in `platform/dashboard` | Hot-reloads the frontend | + +Open [http://localhost:3000](http://localhost:3000) and sign in with the credentials from `.env`. + +:::info First run +The first `pnpm dev` pulls Neo4j and PostgreSQL images — this takes a minute or two. Subsequent starts are fast because Docker caches the images. +::: + +--- + +## Running tests + +```bash +# Unit tests (from platform/core) +cd platform/core && pnpm run test + +# Watch mode +pnpm run test:watch + +# With coverage +pnpm run test:cov +``` + +E2E tests require a running Neo4j + RushDB instance — see the test README in `packages/javascript-sdk`. + +--- + +## Production build + +```bash +# From repo root +pnpm build + +# Then from platform/core +cd platform/core && pnpm run start:prod +``` + +The compiled output goes to `platform/core/dist/`. For production deployments prefer the Docker image — it includes the correct Node.js version and all runtime dependencies. diff --git a/docs/docs/deploy/local-hosting/helm.mdx b/docs/docs/deploy/local-hosting/helm.mdx new file mode 100644 index 00000000..c7455a43 --- /dev/null +++ b/docs/docs/deploy/local-hosting/helm.mdx @@ -0,0 +1,196 @@ +--- +slug: /deploy/local-helm +title: Helm +description: Deploy RushDB on Kubernetes using the official Helm chart — values reference, Neo4j wiring, ingress, and persistence. +sidebar_position: 3 +displayed_sidebar: docs +tags: [Getting Started, Deployment, Kubernetes, Helm] +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# Helm + +Deploy RushDB on Kubernetes using the Helm chart. The chart bundles RushDB and optionally a Neo4j instance — or you can point it at an existing Neo4j deployment. + +## Prerequisites + +| Tool | Minimum version | +| ----------------- | :-------------------------------: | +| Kubernetes | 1.27 | +| Helm | 3.12 | +| Neo4j + APOC Core | 2026.x (see [note below](#neo4j)) | + +--- + +## Install + + + + +```bash +helm install rushdb oci://ghcr.io/rush-db/charts/rushdb \ + --set rushdb.encryptionKey="32SymbolStringForTokenEncryption" \ + --set rushdb.adminLogin=admin \ + --set rushdb.adminPassword=changeme \ + --set neo4j.enabled=true \ + --set neo4j.password=neo4jpassword +``` + + + + +```bash +helm install rushdb oci://ghcr.io/rush-db/charts/rushdb \ + --set rushdb.encryptionKey="32SymbolStringForTokenEncryption" \ + --set rushdb.adminLogin=admin \ + --set rushdb.adminPassword=changeme \ + --set neo4j.enabled=false \ + --set rushdb.neo4jUrl=bolt://your-neo4j-host:7687 \ + --set rushdb.neo4jUsername=neo4j \ + --set rushdb.neo4jPassword=your-password +``` + + + + +:::warning Encryption key +`rushdb.encryptionKey` must be **exactly 32 characters**. This value encrypts all API token values at rest — do not change it after the first install without a migration. +::: + +--- + +## values.yaml reference + +Create a `values.yaml` for repeatable installs: + +```yaml +rushdb: + image: + repository: rushdb/platform + tag: latest # pin to a specific version in production + pullPolicy: IfNotPresent + + # Required + encryptionKey: '32SymbolStringForTokenEncryption' # exactly 32 chars + adminLogin: admin + adminPassword: changeme + + # Neo4j connection (when neo4j.enabled=false) + neo4jUrl: '' + neo4jUsername: neo4j + neo4jPassword: '' + + # SQL metadata database + sqlDbType: sqlite # or "postgres" + sqlDbUrl: '' # postgresql://user:pass@host:5432/rushdb (when postgres) + + service: + type: ClusterIP + port: 3000 + + ingress: + enabled: false + # className: nginx + # annotations: {} + # hosts: + # - host: rushdb.example.com + # paths: + # - path: / + # pathType: Prefix + # tls: [] + + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + + persistence: + enabled: true + size: 1Gi + # storageClass: "" + +neo4j: + enabled: true + image: neo4j:2026.01.4 + password: neo4jpassword + plugins: + - apoc + persistence: + enabled: true + size: 10Gi + resources: + requests: + memory: 2Gi + limits: + memory: 4Gi +``` + +Apply with: + +```bash +helm install rushdb oci://ghcr.io/rush-db/charts/rushdb -f values.yaml +``` + +--- + +## Upgrade + +```bash +helm upgrade rushdb oci://ghcr.io/rush-db/charts/rushdb -f values.yaml +``` + +--- + +## Expose via Ingress + +Enable the ingress in `values.yaml`: + +```yaml +rushdb: + ingress: + enabled: true + className: nginx + hosts: + - host: rushdb.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: rushdb-tls + hosts: + - rushdb.example.com +``` + +--- + +## Neo4j + +The bundled Neo4j sub-chart uses `neo4j:2026.01.4` with APOC Core pre-installed. This is the same version used in the Docker Compose setup. + +For production workloads consider: + +- **Neo4j Aura** — managed Neo4j with automatic backups. Set `neo4j.enabled=false` and configure `rushdb.neo4jUrl` with a `neo4j+s://` URI. +- **Neo4j Helm chart** — the [official Neo4j Helm chart](https://neo4j.com/docs/operations-manual/current/kubernetes/helm-charts-overview/) for fine-grained resource control. + +:::info APOC Core required +RushDB will not start without APOC Core. If you bring your own Neo4j instance, ensure `apoc-2026.x.x-core.jar` is installed and the version matches your Neo4j release. +::: + +--- + +## Uninstall + +```bash +helm uninstall rushdb +``` + +PersistentVolumeClaims are not deleted automatically. To remove them: + +```bash +kubectl delete pvc -l app.kubernetes.io/instance=rushdb +``` diff --git a/docs/docs/deploy/local-hosting/index.mdx b/docs/docs/deploy/local-hosting/index.mdx new file mode 100644 index 00000000..9ad10a76 --- /dev/null +++ b/docs/docs/deploy/local-hosting/index.mdx @@ -0,0 +1,60 @@ +--- +slug: /deploy/local-index +title: Local Installation +description: Run RushDB on your machine. Choose between Docker, running from source, or a Kubernetes Helm chart. +sidebar_position: 1 +displayed_sidebar: docs +overview_page: true +tags: [Getting Started, Deployment, Docker] +--- + +import { DeployMethodRow, DeployMethodRowList, DeployCrossLinks } from '@site/src/components/DeployMethodCard' + +# Local Installation + +Run RushDB on your machine. Pick the option that fits your workflow. + + + + + + + + diff --git a/docs/docs/deploy/remote-hosting/aws-gcp-azure.mdx b/docs/docs/deploy/remote-hosting/aws-gcp-azure.mdx new file mode 100644 index 00000000..81187655 --- /dev/null +++ b/docs/docs/deploy/remote-hosting/aws-gcp-azure.mdx @@ -0,0 +1,80 @@ +--- +slug: /deploy/remote-cloud-providers +title: AWS / GCP / Azure +description: Deploy RushDB on major cloud providers — AWS EC2, Google Cloud Compute Engine, or Azure Virtual Machines. +sidebar_position: 3 +displayed_sidebar: docs +tags: [Deployment, AWS, GCP, Azure, Self-Hosted] +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# AWS / GCP / Azure + +RushDB runs on any Linux VM with Docker. The workflow is the same across providers — provision a VM, install Docker, deploy the Compose stack. + +Verify the [Prerequisites](/deploy/remote-prerequisites) first. + +## Provision a VM + + + + +1. Launch an **EC2 instance** — `t3.medium` (4 GB RAM) or larger +2. Use **Ubuntu 22.04 LTS** AMI +3. Security Group inbound rules: + - `22` (SSH) — your IP only + - `3000` (RushDB API) — your IP or 0.0.0.0/0 + - `80` / `443` (reverse proxy) — 0.0.0.0/0 + + + + +1. Create a **Compute Engine** VM — `e2-medium` (4 GB RAM) or larger +2. Use **Ubuntu 22.04 LTS** boot disk +3. Firewall rules (VPC Network → Firewall): + - Allow ingress on `tcp:3000` for the VM's tag + - Allow ingress on `tcp:80,443` for public traffic + + + + +1. Create a **Virtual Machine** — `B2s` (2 vCPUs, 4 GB) or larger +2. Use **Ubuntu 22.04 LTS** image +3. Network Security Group inbound rules: + - Port `22` — SSH + - Port `3000` — RushDB API + - Port `80` / `443` — reverse proxy + + + + +## Install Docker & Deploy + +After SSHing into your VM, the deployment steps are identical across all providers: + +```bash +# Install Docker +curl -fsSL https://get.docker.com | sh + +# Download and configure RushDB +mkdir rushdb && cd rushdb +curl -O https://raw.githubusercontent.com/rush-db/rushdb/main/platform/docker-compose.yml +curl -O https://raw.githubusercontent.com/rush-db/rushdb/main/.env.example +cp .env.example .env + +# Edit .env with your settings, then start +docker compose up -d +``` + +See [Environment Variables](/deploy/config-env-variables) for the full `.env` reference. + +## Verify + +```bash +curl http://VM_PUBLIC_IP:3000/health +# → {"status":"ok"} +``` + +Next: [Get your API Key](/deploy/get-api-key) to start using the instance. diff --git a/docs/docs/deploy/remote-hosting/digital-ocean.mdx b/docs/docs/deploy/remote-hosting/digital-ocean.mdx new file mode 100644 index 00000000..2dffc6d7 --- /dev/null +++ b/docs/docs/deploy/remote-hosting/digital-ocean.mdx @@ -0,0 +1,88 @@ +--- +slug: /deploy/remote-digital-ocean +title: DigitalOcean +description: Deploy RushDB on a DigitalOcean Droplet with Docker Compose. +sidebar_position: 2 +displayed_sidebar: docs +tags: [Deployment, DigitalOcean, Self-Hosted] +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# DigitalOcean + +Deploy RushDB on a DigitalOcean Droplet using Docker Compose. The steps below assume a fresh Ubuntu 22.04 droplet. + +## 1. Create a Droplet + +1. Log in to [cloud.digitalocean.com](https://cloud.digitalocean.com) +2. Create a new Droplet — **Ubuntu 22.04 LTS**, minimum **4 GB RAM / 2 vCPU** (Basic $24/mo or higher) +3. Add your SSH key during creation + +Verify the [Prerequisites](/deploy/remote-prerequisites) before continuing. + +## 2. Install Docker + +```bash +# SSH into your droplet +ssh root@YOUR_DROPLET_IP + +# Install Docker + Compose plugin +curl -fsSL https://get.docker.com | sh +docker --version +docker compose version +``` + +## 3. Deploy RushDB + +```bash +# Download the production Compose file +mkdir rushdb && cd rushdb +curl -O https://raw.githubusercontent.com/rush-db/rushdb/main/platform/docker-compose.yml +curl -O https://raw.githubusercontent.com/rush-db/rushdb/main/.env.example + +# Configure environment +cp .env.example .env +nano .env # set NEO4J_PASSWORD, ENCRYPTION_KEY, etc. + +# Start +docker compose up -d +``` + +See [Environment Variables](/deploy/config-env-variables) for the full configuration reference. + +## 4. Configure Firewall + +```bash +# DigitalOcean UFW setup +ufw allow OpenSSH +ufw allow 3000 # RushDB API +ufw allow 80 # HTTP (for reverse proxy) +ufw allow 443 # HTTPS (for reverse proxy) +ufw enable +``` + +## 5. (Optional) Add a Reverse Proxy + +Use Caddy for automatic TLS: + +```bash +apt install -y caddy + +# /etc/caddy/Caddyfile +# api.yourdomain.com { +# reverse_proxy localhost:3000 +# } + +systemctl reload caddy +``` + +## Verify + +```bash +curl http://YOUR_DROPLET_IP:3000/health +# → {"status":"ok"} +``` + +Next: [Get your API Key](/deploy/get-api-key) to start using the instance. diff --git a/docs/docs/deploy/remote-hosting/index.mdx b/docs/docs/deploy/remote-hosting/index.mdx new file mode 100644 index 00000000..3b0e6f42 --- /dev/null +++ b/docs/docs/deploy/remote-hosting/index.mdx @@ -0,0 +1,69 @@ +--- +slug: /deploy/remote-index +title: Remote Hosting +description: Deploy RushDB to a remote server. Choose your cloud provider or run Docker on any Linux machine. +sidebar_position: 2 +displayed_sidebar: docs +overview_page: true +tags: [Deployment, Production, Self-Hosted] +--- + +import { DeployMethodRow, DeployMethodRowList, DeployCrossLinks } from '@site/src/components/DeployMethodCard' + +# Remote Hosting + +Deploy RushDB to any server. All methods use Docker Compose — pick the guide for your environment. + + + + + + + + + diff --git a/docs/docs/deploy/remote-hosting/prerequisites.mdx b/docs/docs/deploy/remote-hosting/prerequisites.mdx new file mode 100644 index 00000000..8ee3e878 --- /dev/null +++ b/docs/docs/deploy/remote-hosting/prerequisites.mdx @@ -0,0 +1,44 @@ +--- +slug: /deploy/remote-prerequisites +title: Prerequisites +description: System requirements and software dependencies before deploying RushDB to a remote server. +sidebar_position: 1 +displayed_sidebar: docs +tags: [Deployment, Prerequisites] +--- + +# Prerequisites + +Before deploying RushDB to any remote environment, verify your server meets the following requirements. + +## System Requirements + +| Resource | Minimum | Recommended | +| -------- | -------------------------------------- | ---------------- | +| CPU | 2 vCPUs | 4 vCPUs | +| RAM | 4 GB | 8 GB | +| Disk | 20 GB SSD | 50 GB SSD | +| OS | Ubuntu 22.04+ / Debian 12+ / any Linux | Ubuntu 22.04 LTS | + +## Software Dependencies + +| Tool | Minimum Version | Notes | +| -------------- | --------------- | ----------------------------------------------------- | +| Docker | 24.0+ | `docker --version` | +| Docker Compose | 2.20+ | Bundled with Docker Desktop; `docker compose version` | + +## Network Requirements + +Open the following ports on your server firewall before starting: + +| Port | Service | Required | +| ------------ | ---------------------------- | -------------------------------------- | +| `3000` | RushDB API | Yes | +| `7474` | Neo4j Browser (HTTP) | Dev only — block in production | +| `7687` | Neo4j Bolt | Internal only — do not expose publicly | +| `5432` | PostgreSQL | Internal only | +| `80` / `443` | Reverse proxy (nginx, Caddy) | Recommended for production | + +## Domain & TLS + +A custom domain with TLS is strongly recommended for any internet-facing deployment. See [Security](/deploy/config-security) for setup guidance. diff --git a/docs/docs/tutorials/deployment.mdx b/docs/docs/deploy/remote-hosting/self-hosting-rushdb.mdx similarity index 94% rename from docs/docs/tutorials/deployment.mdx rename to docs/docs/deploy/remote-hosting/self-hosting-rushdb.mdx index 475bd57a..b9b8de7e 100644 --- a/docs/docs/tutorials/deployment.mdx +++ b/docs/docs/deploy/remote-hosting/self-hosting-rushdb.mdx @@ -1,4 +1,5 @@ --- +slug: /deploy/docker-remote sidebar_position: 3 title: 'Self-Hosting RushDB' description: Deploy RushDB on your own infrastructure with Docker Compose, connect your own Neo4j instance, and configure embedding support. @@ -74,7 +75,7 @@ services: RUSHDB_LOGIN: admin RUSHDB_PASSWORD: change-me-in-production - # Encryption key — must be exactly 32 characters + # Encryption key — generate: openssl rand -hex 32 RUSHDB_AES_256_ENCRYPTION_KEY: '32-char-key-change-in-production' # Neo4j @@ -134,7 +135,7 @@ rushdb: SQL_DB_URL: postgresql://rushdb:rushdb-pg-password@postgres:5432/rushdb ``` -See [Connecting an Aura Instance](./connect-aura-instance.mdx) for the full BYOC walkthrough. +See [Connecting an Aura Instance](/deploy/connect-aura) for the full BYOC walkthrough. --- @@ -306,7 +307,7 @@ docker exec rushdb rushdb update-password admin@example.com newsecurepassword456 | Item | Action | | ------------------------------- | ---------------------------------------------------------------------------------------- | | `RUSHDB_PASSWORD` | Set a strong password — never leave `password` | -| `RUSHDB_AES_256_ENCRYPTION_KEY` | Generate a random 32-character string | +| `RUSHDB_AES_256_ENCRYPTION_KEY` | Run `openssl rand -hex 32` | | Neo4j password | Change from the Compose default | | TLS | Terminate TLS at a reverse proxy (nginx, Caddy, Traefik) in front of port 3000 | | Postgres | Back up the `rushdb` Postgres database — it stores projects, tokens, and billing records | @@ -317,6 +318,6 @@ docker exec rushdb rushdb update-password admin@example.com newsecurepassword456 ## Next steps -- [Project Setup After Deployment](./self-hosted-project-setup.mdx) — create projects, invite team members, configure per-project embedding models -- [Connecting an Aura Instance](./connect-aura-instance.mdx) — use an existing Neo4j Aura database as the RushDB data store -- [BYOC vs Managed vs Self-hosted](./byoc-vs-managed.mdx) — choose the right deployment topology for your use case +- [Project Setup After Deployment](/deploy/self-hosted-project-setup) — create projects, invite team members, configure per-project embedding models +- [Connecting an Aura Instance](/deploy/connect-aura) — use an existing Neo4j Aura database as the RushDB data store +- [BYOC vs Managed vs Self-hosted](/tutorials/byoc-vs-managed) — choose the right deployment topology for your use case diff --git a/docs/docs/get-started/_category_.json b/docs/docs/get-started/_category_.json deleted file mode 100644 index 6a43331c..00000000 --- a/docs/docs/get-started/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Get Started", - "position": 1, - "collapsed": false, - "collapsible": false -} diff --git a/docs/docs/get-started/get-api-key.mdx b/docs/docs/get-started/get-api-key.mdx deleted file mode 100644 index 39834033..00000000 --- a/docs/docs/get-started/get-api-key.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Get API Key -description: Learn how to get your RushDB API token -sidebar_position: 2 ---- - -# Get API Key - -To use RushDB, you'll need an API token for authentication. Here's how to get one: - -## 1. Create an Account - -Visit [RushDB Dashboard](https://app.rushdb.com) and sign up for an account if you haven't already. - -## 2. Create a Project - -1. After signing in, click on "New Project" -2. Enter a name for your project -3. Click "Create" - -## 3. Generate API key - -1. In your project dashboard, navigate to the "API Keys" section -2. Click "Generate New API Key" -3. Give your token a name (e.g., "Development", "Production") -4. Click "Generate Key" - -## 4. Copy and Store Your API Key - -Your API token will be displayed only once. Make sure to: - -1. Copy the token immediately -2. Store it securely -3. Never commit it to version control -4. Use environment variables or secure configuration management - -Example of using environment variables: - -import Tabs from '@site/src/components/LanguageTabs' -import TabItem from '@theme/TabItem' - - - -```python -import os - -db = RushDB(api_key=os.environ['RUSHDB_API_KEY']) - -```` - - -```typescript -// RushDB reads RUSHDB_API_KEY from the environment automatically -const db = new RushDB(); - -// Or pass it explicitly: -// const db = new RushDB(process.env.RUSHDB_API_KEY); -```` - - - -```bash -# Set in your shell -export RUSHDB_API_KEY='your-api-key' - -# Use in requests - -curl -H "Authorization: Bearer $RUSHDB_API_KEY" \ - https://api.rushdb.com/api/v1/records - -``` - - - -## Token Security - -- Keep your tokens secure and private -- Rotate tokens periodically -- Use different tokens for development and production -- Revoke tokens immediately if compromised - -## Next Steps - -- Follow the [Quick Tutorial](../get-started/quick-tutorial) to start using your token -- Connect an AI agent via the [MCP Server](../mcp-server/quickstart) -- Bootstrap agent memory with the [Agent Setup guide](https://rushdb.com/agent-setup) -- Learn about [RushDB](../concepts/records) -- Check out the [Basic Concepts](../concepts/records) -``` diff --git a/docs/docs/get-started/quick-tutorial.mdx b/docs/docs/get-started/quick-tutorial.mdx index 9ab42a20..79eb1344 100644 --- a/docs/docs/get-started/quick-tutorial.mdx +++ b/docs/docs/get-started/quick-tutorial.mdx @@ -2,6 +2,7 @@ title: Quick Start description: The fastest path to RushDB — pick your preferred tool. sidebar_position: 1 +displayed_sidebar: docs --- # Quick Start @@ -15,7 +16,7 @@ RushDB is a graph database built for agents and structured data. Pick the path t | [Skills](#copilot-skills) | Any MCP-capable agent in VS Code | ~2 min | | [SDK / REST](#sdk) | Custom apps and pipelines | ~5 min | -Need an API key first? → [Get API Key](./get-api-key) +Need an API key first? → [Get API Key](/deploy/get-api-key) --- @@ -39,7 +40,7 @@ Then verify the connection: > "Call getOntologyMarkdown and show me what's in my RushDB project." -→ [Full MCP client list and options](../mcp-server/quickstart) +→ [Full MCP client list and options](/connect/mcp) --- @@ -108,12 +109,13 @@ Install the RushDB skills to give your agent structured domain knowledge — so Compatible with Claude, GitHub Copilot, Cursor, Windsurf, and any [Agent Skills](https://agentskills.io/)-compatible client. -| Skill | What it enables | -| ----------------------- | ---------------------------------------------------------------- | -| `rushdb-agent-memory` | Store sessions, decisions, and context; recall by meaning | -| `rushdb-query-builder` | Build `findRecords` filters, aggregations, and semantic searches | -| `rushdb-data-modeling` | Design labels, properties, relationships, and nested schemas | -| `rushdb-faceted-search` | Build faceted filter UIs from property metadata | +| Skill | What it enables | +| ------------------------ | ---------------------------------------------------------------- | +| `rushdb-agent-memory` | Store sessions, decisions, and context; recall by meaning | +| `rushdb-query-builder` | Build `findRecords` filters, aggregations, and semantic searches | +| `rushdb-data-modeling` | Design labels, properties, relationships, and nested schemas | +| `rushdb-faceted-search` | Build faceted filter UIs from property metadata | +| `rushdb-domain-template` | Design a schema for any domain through guided conversation | **Install:** @@ -195,7 +197,7 @@ curl -X POST "https://api.rushdb.com/api/v1/records/import/json" \ -→ [TypeScript SDK](../typescript-sdk/introduction) · [Python SDK](../python-sdk/introduction) · [REST API](../rest-api/introduction) +→ [TypeScript SDK](/reference/typescript) · [Python SDK](/reference/python) · [REST API](../rest-api/introduction) --- @@ -220,4 +222,4 @@ Three ideas explain how RushDB works: - **Labels** — the record's type (`SESSION`, `DECISION`, `ARTICLE`). You define them by pushing data. - **Relationships** — named, directed edges between records. Auto-created from nested JSON. -→ [Data model overview](../concepts/data-ingestion.mdx) · [Relationships](../concepts/relationships.mdx) · [Search](../concepts/search/introduction.md) +→ [Data model overview](/build/data/import-data) · [Relationships](/build/graph) · [Search](/reference/search-query) diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx deleted file mode 100644 index e6a03d82..00000000 --- a/docs/docs/index.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -id: index -title: Introduction -sidebar_position: 0 -hide_title: true -hide_table_of_contents: true -hide_breadcrumbs: true -pagination_next: null -pagination_prev: null ---- -import DocsHomePage from '@site/src/components/DocsHomePage'; - - diff --git a/docs/docs/learn/agent-memory/index.mdx b/docs/docs/learn/agent-memory/index.mdx new file mode 100644 index 00000000..81f445ac --- /dev/null +++ b/docs/docs/learn/agent-memory/index.mdx @@ -0,0 +1,114 @@ +--- +slug: /build/agent-memory/ +sidebar_position: 1 +title: Agent Memory Overview +--- + +# Agent Memory Overview + +RushDB is designed from the ground up as a **structured memory store for AI agents**. Unlike flat vector databases or document stores, RushDB gives agents three distinct memory layers — each addressing a different dimension of knowledge — and a retrieval stack that composes them at query time. + +--- + +## One Primitive, Any Architecture + +Most AI memory solutions ship with a predefined set of entity types — `User`, `Memory`, `Fact`, `Episode` — and a retrieval pipeline built around those types. If your use case fits the mold, you're productive quickly. If it doesn't, you spend time working around the framework rather than building your product. + +RushDB takes the opposite approach. It gives you exactly **one primitive**: Record. You decide what a Record represents, what properties it carries, and how Records relate to each other. The schema emerges from your problem — not from an opinion baked into the library. + +A **chat assistant** stores `Message`, `Session`, and `User` records wired by conversation threads. A **long-running medical research harness** with decentralized inference stores `Patient`, `ClinicalNote`, `Observation`, `InferenceRun`, and `ModelVersion` records with full provenance relationships. A **multi-agent coordination layer** tracks `Agent`, `Task`, `Artifact`, and `Delegation` records across independent inference nodes. Every architecture is different — and every architecture is built from the same two building blocks: **Records** and **Relationships**. + +Because there's no fixed schema to lock you in, you can change, extend, or completely rethink your data model at any time with no migrations and no downtime. + +--- + +## The Three Memory Layers + +| Layer | What it stores | RushDB primitive | +| -------------- | ----------------------------------------------------------------------- | ------------------------------ | +| **Episodic** | Individual facts, events, entities, and their connections | Records + Relationships | +| **Semantic** | Meaning encoded as dense vectors (embeddings) | Vector Properties + AI Indexes | +| **Structural** | Schema: what labels exist, what properties they carry, how they connect | Ontology API | + +### Episodic Memory — Records and Relationships + +Every discrete piece of knowledge is a **Record**: a typed key-value object carrying a label, properties, and a system-generated ID. Records connect to one another via **Relationships**, forming a traversable knowledge graph. An agent can store anything from a conversation turn to a product entity as a record, then retrieve it by property values, label, or graph traversal. + +→ [Store Records](/build/data/store-records) · [Connect Records](/build/graph/connect-records) + +### Semantic Memory — Vector Properties + +A subset of record properties carry dense vector representations (embeddings). A vector-indexed property is simultaneously a semantic index over every record it connects to. Embeddings can be supplied by the application (Bring Your Own Vector) or generated automatically by RushDB. + +→ [Manage Embedding Indexes](/build/ai-search/manage-indexes) · [Semantic Search](/build/ai-search/semantic-search) + +### Structural Memory — Ontology + +The **Ontology API** returns a live snapshot of the graph's schema: all labels, all properties per label with types and value ranges, and the full relationship map. An agent calls this once per session to bootstrap awareness of what is in the database — no hardcoded schema, no external documentation required. + +→ [Discover Your Schema](/build/schema/discover-your-schema) · [Schema Self-Awareness](/build/agent-memory/schema-self-awareness) + +--- + +## The Retrieval Stack + +A well-designed agent retrieval pipeline uses all three layers in sequence: + +``` +Agent + → 1. Ontology discovery (structural) + → 2. Faceted filter (structural — fast, deterministic) + → 3. Semantic re-rank (semantic — surfaces most relevant) + → 4. Structured results returned to agent +``` + +**Step 1 — Discover the schema.** Before constructing any query, the agent calls the ontology endpoint to learn what labels, properties, and relationships exist. This prevents hallucinated field names and enables dynamic query construction. + +**Step 2 — Filter structurally.** The `where` clause narrows the candidate set by exact or range conditions on scalar properties. Fast (index-backed) and deterministic. + +**Step 3 — Re-rank semantically.** After structural filtering, similarity scoring surfaces the most _relevant_ records from the structurally valid candidate set. + +**Step 4 — Return to agent.** The sorted, scored result set is returned. The agent reasons over structured records — not raw text chunks — because RushDB preserves full property context alongside the similarity score. + +--- + +## Retrieval Approach Comparison + +| Approach | When to use | Mechanism | +| -------------------------------------- | ---------------------------------------------------------------- | ----------------------------------- | +| **Structural only** | Known labels and property values | `where` filter | +| **Semantic with pre-filter** | Meaning-based lookup, optionally scoped by structural conditions | `db.ai.search()` with `where` | +| **Structural + semantic in one query** | Full SearchQuery features alongside similarity scoring | `where` + `$similarity` aggregation | + +--- + +## Self-Awareness Without External Documentation + +A central design goal of RushDB is that agents should be able to operate against an unknown or evolving knowledge graph **without any out-of-band schema documentation**. Two mechanisms make this possible: + +1. **`__proptypes`** — every Record carries a `__proptypes` field listing the name and type of each property it holds. This makes every record self-describing. + +2. **The Ontology API** — aggregates all `__proptypes` metadata across the project and returns it as a schema snapshot. An agent that calls the ontology endpoint at the start of a session receives the full graph schema in a single, token-efficient Markdown string. + +``` +Boot → call ontology → understand what exists in the graph + → construct query from real labels/properties + → retrieve relevant records + → act on structured, typed results +``` + +--- + +## What's Next + +The rest of the Learn section covers the building blocks and query primitives that make this memory model work: + +| Section | What you'll learn | +| ------------------------------------------------------------------ | ----------------------------------------------------------------------------- | +| [Quickstart](/build/agent-memory/quickstart) | Connect the MCP server, run the bootstrap prompt, validate in 5 min | +| [Schema Self-Awareness](/build/agent-memory/schema-self-awareness) | Ontology API, `__proptypes`, dynamic query construction | +| [Records & Queries](/build/data/store-records) | Store, import, update, delete, and search records | +| [SearchQuery reference](/reference/search-query) | `where`, `select`, `groupBy`, `orderBy`, pagination — the full query language | +| [Relationships](/build/graph/connect-records) | Attach edges between records, bulk-connect by key, traverse in queries | +| [Semantic Search](/build/ai-search/semantic-search) | Meaning-based recall — embedding indexes, similarity scoring, pre-filter | +| [Transactions](/build/reliability/transactions) | ACID guarantees for multi-step memory writes | diff --git a/docs/docs/learn/agent-memory/quickstart.mdx b/docs/docs/learn/agent-memory/quickstart.mdx new file mode 100644 index 00000000..52fec5d7 --- /dev/null +++ b/docs/docs/learn/agent-memory/quickstart.mdx @@ -0,0 +1,303 @@ +--- +slug: /build/agent-memory/quickstart +sidebar_position: 1 +title: Agent Memory Quickstart +--- + +import Tabs from '@theme/TabItem' + +# Agent Memory Quickstart + +Give any agent a persistent, graph-structured memory in under five minutes. Sessions, decisions, tasks, entities, and preferences — stored in RushDB, traversable by meaning. + +--- + +## Prerequisites + +1. **API key** — sign up free at [app.rushdb.com](https://app.rushdb.com) and copy your project API key. +2. **A running RushDB instance** — [cloud](https://app.rushdb.com) or [self-hosted](/deploy/local-docker). + +--- + +## Distribution URL + +Any agent or automation can fetch the full setup guide programmatically: + +``` +https://rushdb.com/agent-setup +``` + +This URL always serves the canonical setup prompt. Point your agent at it instead of copy-pasting: + +``` +Fetch https://rushdb.com/agent-setup and follow the instructions exactly. +``` + +--- + +## Step 1 — Connect the MCP Server + +Add the `@rushdb/mcp-server` to your agent's MCP config, then restart. + +import Tabs2 from '@theme/TabItem' + +import TabsHost from '@theme/Tabs' + + + + +Edit `~/Library/Application Support/Claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "rushdb": { + "command": "npx", + "args": ["@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "" + } + } + } +} +``` + +Restart Claude Desktop. The RushDB tools appear in the tool list automatically. + + + + +Add to your MCP settings (`.vscode/mcp.json` or user-level MCP config): + +```json +{ + "servers": { + "rushdb": { + "command": "npx", + "args": ["@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "" + } + } + } +} +``` + +For **GitHub Copilot agent mode**: add the block above and reload the window. RushDB tools are available to the agent in `@workspace` sessions. + +For **OpenClaw**: add the same config to your OpenClaw workspace settings, or install via the OpenClaw skill registry (search `rushdb`). + + + + +```bash +# Set the key once +export RUSHDB_API_KEY= + +# Add to your Codex MCP config (~/.codex/mcp.json or project-level) +``` + +```json +{ + "mcpServers": { + "rushdb": { + "command": "npx", + "args": ["@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "" + } + } + } +} +``` + +Then run Codex normally — it will discover the RushDB tools at startup. + + + + +Hermes-based agents that support MCP can use the same npx transport: + +```json +{ + "mcp_servers": { + "rushdb": { + "transport": "stdio", + "command": "npx", + "args": ["@rushdb/mcp-server"], + "env": { + "RUSHDB_API_KEY": "" + } + } + } +} +``` + +If your Hermes host uses a different MCP config key, replace `mcp_servers` with the correct key — the inner object is the same. + + + + +Run the server as a sidecar and expose it via SSE: + +```bash +docker run -d \ + -e RUSHDB_API_KEY= \ + -p 3001:3001 \ + rushdb/mcp-server +``` + +Point your agent at `http://localhost:3001/sse` as the MCP SSE transport. + + + + +--- + +## Step 2 — Install the Agent Skills + +Install the RushDB skills pack so your agent knows how to use the MCP tools for memory, querying, and data modeling: + +```bash +npx skills add rush-db/rushdb --path packages/skills +``` + +Or install the package with npm: + +```bash +npm install @rushdb/skills +``` + +The pack includes: + +| Skill | What it enables | +| ---------------------- | ----------------------------------------------------------------------- | +| `rushdb-agent-memory` | Store sessions, decisions, tasks, and preferences; recall prior context | +| `rushdb-query-builder` | Build correct filters, traversals, aggregations, and semantic searches | +| `rushdb-data-modeling` | Design labels, properties, relationships, and nested memory schemas | + +:::note MCP server vs. Agent Skills +The MCP server gives your agent RushDB tools at runtime. Agent Skills teach it how to use those tools correctly. Install both, then start a new agent session so the skills are discovered. +::: + +For OpenClaw workspace-level installation, see [Using RushDB Agent Skills in OpenClaw](/tutorials/agent-skills-with-openclaw#part-2--install-the-rushdb-skills-pack). + +--- + +## Step 3 — Bootstrap the Agent + +After the MCP server is connected, send this prompt once per new agent / project: + +``` +Set up RushDB as my persistent memory layer. Follow these steps in order. + +INITIALIZE +1. Call getOntologyMarkdown to check existing memory labels and record counts. +2. If SESSION labels already exist, call findRecords with labels:["SESSION"], + orderBy:{startedAt:"desc"}, limit:1 and summarize the most recent session. +3. Create a SESSION record for this conversation: + { + "label": "SESSION", + "data": { + "startedAt": "", + "topic": "", + "agentId": "" + } + } + +IMPORT EXISTING IDENTITY (if applicable) +4. Check if any of these files exist and read them: + - SOUL.md, IDENTITY.md, USER.md, MEMORY.md + - ~/.openclaw/workspace/memory/ (most recent 30 days of YYYY-MM-DD.md) + - Any preference or identity files the user points you to. +5. Extract facts and create records: + - User preferences → label "PREFERENCE" + - Identity facts → label "ENTITY" with type:"identity" + - Past decisions → label "DECISION" + - Ongoing tasks → label "TASK" + +CONFIRM +6. Call getOntologyMarkdown again and report what labels exist with record counts. +7. Run a test recall: findRecords with labels:["PREFERENCE"] limit:5 +``` + +--- + +## Step 4 — Validate + +Ask the agent: + +``` +Search RushDB for any SESSION records. Show me the most recent one. +``` + +If it returns a session record, memory is live. From here the agent will: + +- Create a new `SESSION` at the start of each conversation +- Write `DECISION`, `TASK`, `PREFERENCE`, and `OBSERVATION` records as it works +- Recall past context with structured queries or semantic search + +--- + +## Recommended Memory Labels + +| Label | What it stores | +| ------------- | ------------------------------------------------------- | +| `SESSION` | A conversation or work session with timestamp and topic | +| `DECISION` | A decision made, with rationale | +| `ENTITY` | A named thing — person, service, project, concept | +| `TASK` | Work item with status and assignee | +| `PREFERENCE` | Persistent user preference or constraint | +| `OBSERVATION` | Raw note or finding | +| `PLAN` | Proposed sequence of steps | +| `ARTIFACT` | A produced output — code snippet, doc, design | + +Nested JSON auto-links records — no manual edge wiring: + +```json +{ + "label": "SESSION", + "data": { + "topic": "auth refactor", + "startedAt": "2026-05-31T09:00:00Z", + "DECISION": [{ "decision": "Switch to Clerk", "rationale": "Better Next.js integration" }], + "TASK": [{ "title": "Remove Auth0 dependency", "status": "pending" }] + } +} +``` + +One `createRecord` call creates `SESSION → DECISION + TASK`, all linked automatically. + +--- + +## Recall Patterns + +Copy these into your agent's system prompt or run them on demand: + +**Last session summary:** + +```json +{ "labels": ["SESSION"], "orderBy": { "startedAt": "desc" }, "limit": 1 } +``` + +**What did we decide about X?** + +```json +{ "labels": ["DECISION"], "where": { "topic": { "$contains": "X" } }, "orderBy": { "decidedAt": "desc" } } +``` + +**Semantic recall (meaning-based):** + +``` +semanticSearch: query="how did we handle auth", labels=["DECISION"], limit=5 +``` + +→ Requires a one-time embedding index. See [Schema Self-Awareness](/build/agent-memory/schema-self-awareness) for setup. + +--- + +## Next Steps + +- [Agent Memory Overview](/build/agent-memory/) — the three memory layers (episodic, semantic, structural) and the full retrieval stack +- [Schema Self-Awareness](/build/agent-memory/schema-self-awareness) — let agents discover and reason about the graph schema dynamically +- [Semantic Search](/build/ai-search/semantic-search) — add meaning-based recall to any memory label diff --git a/docs/docs/learn/agent-memory/schema-self-awareness.mdx b/docs/docs/learn/agent-memory/schema-self-awareness.mdx new file mode 100644 index 00000000..760804e5 --- /dev/null +++ b/docs/docs/learn/agent-memory/schema-self-awareness.mdx @@ -0,0 +1,242 @@ +--- +slug: /build/agent-memory/schema-self-awareness +sidebar_position: 2 +title: Schema Self-Awareness +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Schema Self-Awareness + +RushDB is **self-aware** — it continuously tracks its own structure and exposes it on demand. Agents use this to operate against an unknown or evolving knowledge graph without any hardcoded schema documentation. + +--- + +## Why Schema Discovery Matters + +Without schema discovery, AI agents hallucinate field names, use wrong label casing, and construct invalid queries. With a single ontology call at session start, the agent knows: + +- Every label and how many records it has +- Every property per label, its type, and sample/range values +- The full relationship map (which labels connect to which) +- Which properties are semantically searchable (have embedding indexes) + +--- + +## Inject Schema into LLM Context + + + + +```python +from rushdb import RushDB + +db = RushDB("RUSHDB_API_KEY") + +# Call once at session start +response = db.ai.get_ontology_markdown() +schema = response.data + +# Inject into LLM system prompt +messages = [ + { + "role": "system", + "content": f"""You are a database assistant for RushDB. + +Here is the current graph schema: + +{schema} + +When the user asks questions, construct SearchQuery filters using ONLY the labels and properties shown above. Never invent field names.""" + }, + {"role": "user", "content": "How many paid orders were placed this month?"} +] +``` + + + + +```typescript +import RushDB from '@rushdb/javascript-sdk' + +const db = new RushDB('RUSHDB_API_KEY') + +// Call once at session start +const { data: schema } = await db.ai.getOntologyMarkdown() + +// Inject into LLM system prompt +const messages = [ + { + role: 'system', + content: `You are a database assistant for RushDB. + +Here is the current graph schema: + +${schema} + +When the user asks questions, construct SearchQuery filters using ONLY the labels and properties shown above. Never invent field names.` + }, + { + role: 'user', + content: 'How many paid orders were placed this month?' + } +] +``` + + + + +```bash +# Get Markdown schema for LLM injection +curl -X POST https://api.rushdb.com/api/v1/ai/ontology/md \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{}' + +# Scope to specific labels (reduces token usage) +curl -X POST https://api.rushdb.com/api/v1/ai/ontology/md \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"labels": ["Order", "User"]}' +``` + + + + +--- + +## Dynamic Query Construction + +Use structured ontology to build queries programmatically — no hardcoded field names: + + + + +```python +# Get structured ontology +ontology = db.ai.get_ontology().data + +# Find a label +order_schema = next(item for item in ontology if item["label"] == "Order") + +# Enumerate all string properties +string_props = [ + p for p in order_schema["properties"] if p["type"] == "string" +] + +# Find properties with semantic indexes ready +searchable = [ + p for p in order_schema["properties"] + if any(idx["status"] == "ready" for idx in p.get("vectorIndexes", [])) +] + +# Look up a property ID for value enumeration +status_prop = next(p for p in order_schema["properties"] if p["name"] == "status") +statuses = db.properties.values(status_prop["id"]) +``` + + + + +```typescript +// Get structured ontology +const { data: ontology } = await db.ai.getOntology() + +// Find a label +const orderSchema = ontology.find((item) => item.label === 'Order')! + +// Enumerate all string properties +const stringProps = orderSchema.properties.filter((p) => p.type === 'string') + +// Find properties with semantic indexes ready +const searchable = orderSchema.properties.filter((p) => + p.vectorIndexes?.some((idx) => idx.status === 'ready') +) + +// Look up a property ID for value enumeration +const statusProp = orderSchema.properties.find((p) => p.name === 'status')! +const { data: statuses } = await db.properties.values({ id: statusProp.id }) +``` + + + + +```bash +# Get full structured JSON ontology +curl -X POST https://api.rushdb.com/api/v1/ai/ontology \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{}' +``` + + + + +--- + +## Ontology Caching + +Both ontology endpoints share a **1-hour cache** per project. The first call after TTL expiry triggers a full graph scan; all subsequent calls within the hour are instant. + + + + +```python +# Force fresh recalculation (bypass 1-hour cache) +response = db.ai.get_ontology_markdown({"force": True}) +``` + + + + +```typescript +// Force fresh recalculation (bypass 1-hour cache) +const { data } = await db.ai.getOntologyMarkdown({ force: true }) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/ontology/md \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"force": true}' +``` + + + + +--- + +## Agent Skills + +`@rushdb/skills` is a collection of installable agent skills that teach any skills-compatible AI assistant (Claude, GitHub Copilot, Cursor, Windsurf, and others) to use RushDB efficiently — without manual system-prompt engineering. + +```bash +npx skills add rush-db/rushdb --path packages/skills +``` + +| Skill | What it teaches | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `rushdb-query-builder` | Discovery-first workflow, SearchQuery syntax, aggregation, relationship traversal, and semantic search | +| `rushdb-agent-memory` | Using RushDB as persistent structured memory — store, link, and semantically recall sessions, decisions, and entities | +| `rushdb-data-modeling` | LMPG model design, label/property naming conventions, nested JSON import, and schema evolution | +| `rushdb-faceted-search` | Build faceted filter UIs — discover properties and types, enumerate distinct values, map to widgets, assemble a live `where` clause | +| `rushdb-domain-template` | Design a tailored schema for any domain through guided conversation — interview, entity/relationship mapping, bootstrap payload, and 10 built-in domain templates | + +Each skill bundles a `SKILL.md` with concise instructions and optional reference files that the agent loads on demand. + +:::note MCP server vs. Agent Skills +The [MCP server](/deploy/mcp-operator-quickstart) gives agents direct tool access to RushDB at runtime. Agent Skills teach agents _how_ to use those tools correctly — they complement each other. +::: + +--- + +## See also + +- [Agent Memory Overview](/build/agent-memory/) — three-layer memory model +- [Discover Your Schema](/build/schema/discover-your-schema) — full ontology API reference +- [Semantic Search](/build/ai-search/semantic-search) — search by meaning +- [Find & Query](/build/data/find-and-query) — search by structure diff --git a/docs/docs/learn/records-and-queries/discover-your-schema.mdx b/docs/docs/learn/records-and-queries/discover-your-schema.mdx new file mode 100644 index 00000000..99ede323 --- /dev/null +++ b/docs/docs/learn/records-and-queries/discover-your-schema.mdx @@ -0,0 +1,319 @@ +--- +slug: /build/schema/discover-your-schema +sidebar_position: 2 +title: Discover Your Schema +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Discover Your Schema + +RushDB is **self-aware** — it continuously understands its own structure: labels, field types, value distributions, relationships, and semantic search readiness. The `db.ai` ontology methods expose this as either Markdown (for LLM injection) or structured JSON (for schema UIs, autocomplete, or property-ID lookups). + +--- + +## Why Agents Need This + +Traditional databases assume the application knows the schema at build time. AI agents face a different reality: + +- The knowledge graph may have been populated by other agents, batch imports, or live event streams. +- The schema drifts over time as new label types and properties appear. +- Agents cannot be pre-programmed with field names they have never seen. + +RushDB's answer is **schema-on-read for agents**: call `getOntologyMarkdown()` once per session and receive the full, current schema as a single response. The agent can then construct valid queries referencing only labels and properties that actually exist — no hallucinated field names. + +### What the Ontology Contains + +| Component | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------------- | +| **Label inventory** | All label names in the project, with record counts | +| **Property manifest per label** | Property name, type, and either sample values (strings/booleans) or a min–max range (numbers/datetimes) | +| **Relationship map** | Which labels connect to which, via which relationship type, and in which direction | +| **Semantic index status** | Which properties have embedding indexes and whether they are ready for `db.ai.search()` | + +--- + +## Get Ontology as Markdown + +The **recommended format for LLM context injection** — compact, token-efficient, and ready to paste into a system prompt. + + + + +`db.ai.get_ontology_markdown()` + +```python +from rushdb import RushDB + +db = RushDB("RUSHDB_API_KEY") + +# Inject into LLM at session start +response = db.ai.get_ontology_markdown() +schema = response.data + +messages = [ + {"role": "system", "content": f"You are a data assistant.\n\n{schema}"}, + {"role": "user", "content": "How many paid orders are there?"} +] + +# Scope to specific labels +order_response = db.ai.get_ontology_markdown({"labels": ["Order"]}) + +# Bypass the 1-hour cache and force a fresh recalculation +fresh_response = db.ai.get_ontology_markdown({"force": True}) +``` + + + + +`db.ai.getOntologyMarkdown()` + +```typescript +// Inject into LLM at session start +const { data: schema } = await db.ai.getOntologyMarkdown() +const messages = [ + { role: 'system', content: `You are a data assistant.\n\n${schema}` }, + { role: 'user', content: 'How many paid orders are there?' } +] + +// Scope to specific labels +const { data: orderSchema } = await db.ai.getOntologyMarkdown({ + labels: ['Order'] +}) + +// Bypass the 1-hour cache and force a fresh recalculation +const { data: freshSchema } = await db.ai.getOntologyMarkdown({ force: true }) +``` + + + + +`POST /api/v1/ai/ontology/md` + +```bash +# Full schema +curl -X POST https://api.rushdb.com/api/v1/ai/ontology/md \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{}' + +# Scoped to specific labels +curl -X POST https://api.rushdb.com/api/v1/ai/ontology/md \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{"labels": ["Order"]}' + +# Force cache bypass +curl -X POST https://api.rushdb.com/api/v1/ai/ontology/md \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{"force": true}' +``` + +#### Request body fields + +| Field | Type | Required | Description | +| -------- | ---------- | -------- | ---------------------------------------------------------------------------- | +| `labels` | `string[]` | no | Restrict output to specific labels. Omit (or pass `[]`) for the full schema. | +| `force` | `boolean` | no | Bypass the 1-hour cache and force a full recalculation. | + + + + +### Example Markdown output + +```text +# Graph Ontology + +## Labels + +| Label | Count | +|-----------|------:| +| `Order` | 1840 | +| `User` | 312 | +| `Product` | 95 | + +--- + +## `Order` (1840 records) + +### Properties + +| Property | Type | Values / Range | Semantic Search | +|-------------|----------|----------------------------------------|--------------------------------| +| `status` | string | `pending`, `paid`, `shipped` (+2 more) | — | +| `total` | number | `4.99`..`2499.00` | — | +| `name` | string | `Widget A`, `Widget B` (+8 more) | `managed` cosine 1536d [ready] | +| `createdAt` | datetime | `2024-01-03`..`2026-02-27` | — | + +### Relationships + +| Type | Direction | Other Label | +|-------------|-----------|-------------| +| `PLACED_BY` | out | `User` | +| `CONTAINS` | out | `Product` | +``` + +:::tip Agent quickstart +Call `get_ontology_markdown()` / `getOntologyMarkdown()` first in every AI session. Without it, models will hallucinate field and label names. +::: + +--- + +## Get Ontology (Structured JSON) + +Returns the same ontology as a structured array — useful for schema UIs, autocomplete, or looking up property IDs for `db.properties.values()`. + + + + +`db.ai.get_ontology()` + +```python +# List all labels with counts +response = db.ai.get_ontology() +for item in response.data: + print(f"{item['label']}: {item['count']} records") + +# Scope to specific labels +book_response = db.ai.get_ontology({"labels": ["Book"]}) +book_schema = book_response.data[0] + +# Get property ID for value enumeration +genre_prop = next(p for p in book_schema["properties"] if p["name"] == "genre") +genres = db.properties.values(genre_prop["id"]) + +# Identify semantically searchable properties +indexed = [p for p in book_schema["properties"] if p.get("vectorIndexes")] +# indexed[0]["vectorIndexes"][0]["status"] == "ready" → queryable with db.ai.search() + +# Bypass the 1-hour cache +fresh_response = db.ai.get_ontology({"force": True}) +``` + + + + +`db.ai.getOntology()` + +```typescript +// List all labels with counts +const { data: ontology } = await db.ai.getOntology() +for (const item of ontology) { + console.log(`${item.label}: ${item.count} records`) +} + +// Get property ID for value enumeration +const { + data: [bookSchema] +} = await db.ai.getOntology({ labels: ['Book'] }) +const genreProp = bookSchema.properties.find((p) => p.name === 'genre') +const { data: genres } = await db.properties.values({ id: genreProp.id }) + +// Identify semantically-searchable properties +const indexed = bookSchema.properties.filter((p) => p.vectorIndexes?.length) +// indexed[0].vectorIndexes[0].status === 'ready' → queryable with db.ai.search() + +// Bypass the 1-hour cache +const { data: fresh } = await db.ai.getOntology({ force: true }) +``` + + + + +`POST /api/v1/ai/ontology` + +```bash +# Full schema (structured JSON) +curl -X POST https://api.rushdb.com/api/v1/ai/ontology \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{}' + +# Scoped to specific labels +curl -X POST https://api.rushdb.com/api/v1/ai/ontology \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{"labels": ["Book"]}' +``` + + + + +### TypeScript types + +```typescript +type OntologyItem = { + label: string + count: number + properties: OntologyProperty[] + relationships: OntologyRelationship[] +} + +type OntologyProperty = { + id: string // use with db.properties.values() + name: string + type: string // 'string' | 'number' | 'boolean' | 'datetime' + values?: Array // up to 10 samples (string/boolean only) + min?: number | string // number/datetime only + max?: number | string + /** Non-empty when embedding indexes exist — property is queryable with db.ai.search() */ + vectorIndexes?: OntologyVectorIndex[] +} + +type OntologyVectorIndex = { + id: string + sourceType: string // 'managed' | 'external' + similarityFunction: string // 'cosine' | 'euclidean' + dimensions: number + status: string // 'pending' | 'indexing' | 'ready' | 'error' + modelKey: string +} + +type OntologyRelationship = { + label: string + type: string + direction: 'in' | 'out' +} +``` + +--- + +## Caching + +:::note Caching +Both methods share a **1-hour cache** per project. The first call after TTL expiry triggers a full graph scan; all subsequent calls within the hour are instant. Pass `{ force: true }` to bypass the cache and trigger an immediate recalculation. +::: + +--- + +## Agent Skills + +`@rushdb/skills` is a collection of [Agent Skills](https://agentskills.io) — installable instructions that teach any skills-compatible AI agent (Claude, GitHub Copilot, Cursor, Windsurf, and others) to use RushDB efficiently, without manual system-prompt engineering. + +```bash +npx skills add rush-db/rushdb --path packages/skills +``` + +| Skill | What it teaches | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `rushdb-query-builder` | Discovery-first workflow, SearchQuery syntax, aggregation, relationship traversal, and semantic search | +| `rushdb-agent-memory` | Using RushDB as persistent structured memory — store, link, and semantically recall sessions, decisions, and entities | +| `rushdb-data-modeling` | LMPG model design, label/property naming conventions, nested JSON import, and schema evolution | +| `rushdb-faceted-search` | Build faceted filter UIs — discover properties and types, enumerate distinct values, map to widgets, assemble a live `where` clause | +| `rushdb-domain-template` | Design a tailored schema for any domain through guided conversation — interview, entity/relationship mapping, bootstrap payload, and 10 built-in domain templates | + +Each skill bundles a `SKILL.md` with concise instructions and optional reference files (like the full SearchQuery spec) that the agent loads on demand. + +:::note MCP server vs. Agent Skills +The [MCP server](/deploy/mcp-operator-quickstart) gives agents direct tool access to RushDB at runtime. Agent Skills teach agents _how_ to use those tools correctly — they complement each other. +::: + +--- + +## See also + +- [Labels & Properties](/build/schema/labels-and-properties) — enumerate schema at a low level +- [Semantic Search](/build/ai-search/semantic-search) — use indexed properties for similarity search +- [Manage Embedding Indexes](/build/ai-search/manage-indexes) — view index status in ontology output diff --git a/docs/docs/learn/records-and-queries/export-data.mdx b/docs/docs/learn/records-and-queries/export-data.mdx new file mode 100644 index 00000000..827f445c --- /dev/null +++ b/docs/docs/learn/records-and-queries/export-data.mdx @@ -0,0 +1,116 @@ +--- +slug: /build/data/export-data +sidebar_position: 4 +title: Export Data +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Export Data + +Export records as CSV. Accepts the same `where`, `orderBy`, `skip`, `limit`, and `labels` parameters as a search query — filtering and sorting apply before export. + +--- + +## Export to CSV + + + + +`db.records.export_csv()` _(if available in SDK version — otherwise use REST directly)_ + +```python +# Use the REST endpoint directly if SDK method is unavailable +import requests + +response = requests.post( + "https://api.rushdb.com/api/v1/records/export/csv", + headers={"Authorization": f"Bearer {api_key}"}, + json={ + "labels": ["PERSON"], + "where": {"age": {"$gt": 25}}, + "orderBy": {"name": "asc"}, + "limit": 1000 + } +) + +csv_content = response.json()["data"]["fileContent"] +with open("output.csv", "w") as f: + f.write(csv_content) +``` + + + + +```typescript +// Use the REST endpoint directly +const response = await fetch('https://api.rushdb.com/api/v1/records/export/csv', { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.RUSHDB_API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + labels: ['PERSON'], + where: { age: { $gt: 25 } }, + orderBy: { name: 'asc' }, + limit: 1000 + }) +}) + +const { data } = await response.json() +// data.fileContent — CSV string with headers on first row +// data.dateTime — export timestamp +``` + + + + +`POST /api/v1/records/export/csv` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/export/csv \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["PERSON"], + "where": {"age": {"$gt": 25}}, + "orderBy": {"name": "asc"}, + "limit": 1000 + }' +``` + + + + +### Request parameters + +| Field | Type | Description | +| --------- | ------------------ | ----------------------------------- | +| `labels` | `string[]` | Restrict to specific labels | +| `where` | `object` | Filter conditions (same as search) | +| `orderBy` | `string \| object` | Sort criteria | +| `skip` | `number` | Pagination offset | +| `limit` | `number` | Max records to return (up to 1 000) | + +### Response + +```json +{ + "success": true, + "data": { + "fileContent": "id,label,name,age,email\n018dfc84...,PERSON,John Doe,30,john@example.com", + "dateTime": "2025-04-23T10:15:32.123Z" + } +} +``` + +`fileContent` is a CSV string with headers on the first row. System properties (`__proptypes` etc.) are stripped automatically. `__id` and `__label` are included as `id` and `label`. + +--- + +## See also + +- [Find & Query](/build/data/find-and-query) — search and filter records before export +- [Import Data](/build/data/import-data) — bring CSV or JSON data in diff --git a/docs/docs/learn/records-and-queries/find-and-query.mdx b/docs/docs/learn/records-and-queries/find-and-query.mdx new file mode 100644 index 00000000..10d41b15 --- /dev/null +++ b/docs/docs/learn/records-and-queries/find-and-query.mdx @@ -0,0 +1,785 @@ +--- +slug: /build/data/find-and-query +sidebar_position: 3 +title: Find & Query +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Find & Query + +RushDB provides four read methods: look up by ID, find one, find unique, or run a full search query with filtering, aggregation, and relationship traversal — all through a unified `SearchQuery` object. + +--- + +## Find by ID + + + + +`db.records.find_by_id()` + +```python +# Single record +movie = db.records.find_by_id("movie-123") + +# Multiple records +movies = db.records.find_by_id(["movie-123", "movie-456"]) +``` + + + + +`db.records.findById()` + +```typescript +const movie = await db.records.findById('movie-id-123') +const movies = await db.records.findById(['id-1', 'id-2', 'id-3']) +``` + +Returns `DBRecordInstance` (single) or `DBRecordsArrayInstance` (array). + + + + +`GET /api/v1/records/:entityId` + +```bash +curl https://api.rushdb.com/api/v1/records/movie-123 \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +--- + +## Find One + +Returns the first matching record, or `null` / `None` if none found. + + + + +`db.records.find_one()` + +```python +movie = db.records.find_one({ + "labels": ["MOVIE"], + "where": {"title": "Inception"} +}) +``` + + + + +`db.records.findOne()` + +```typescript +const movie = await db.records.findOne({ + labels: ['MOVIE'], + where: { title: 'Inception' } +}) +``` + + + + +Use `POST /api/v1/records/search` with `"limit": 1`. + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"labels": ["MOVIE"], "where": {"title": "Inception"}, "limit": 1}' +``` + + + + +--- + +## Find Unique + +Like `find_one` / `findOne`, but throws `NonUniqueResultError` if more than one record matches. + + + + +`db.records.find_uniq()` + +```python +movie = db.records.find_uniq({ + "labels": ["MOVIE"], + "where": {"title": "Inception"} +}) +``` + + + + +`db.records.findUniq()` + +```typescript +import { NonUniqueResultError } from '@rushdb/javascript-sdk' + +try { + const movie = await db.records.findUniq({ + labels: ['MOVIE'], + where: { title: 'Inception' } + }) +} catch (e) { + if (e instanceof NonUniqueResultError) console.error(`found ${e.count} matches`) +} +``` + + + + +Not a dedicated endpoint — check `total` from a search response to verify uniqueness. + + + + +--- + +## Search Records + +Full search with filtering, sorting, and pagination. + + + + +`db.records.find()` + +```python +result = db.records.find({ + "labels": ["MOVIE"], + "where": {"rating": {"$gte": 8}, "genre": "sci-fi"}, + "orderBy": {"rating": "desc"}, + "limit": 20, + "skip": 0 +}) + +for movie in result: + print(movie.get("title"), movie.get("rating")) + +print(f"{len(result)} shown, {result.total} total") +``` + + + + +`db.records.find()` + +```typescript +const { data: movies, total } = await db.records.find({ + labels: ['MOVIE'], + where: { rating: { $gte: 8 }, genre: 'sci-fi' }, + orderBy: { rating: 'desc' }, + limit: 20, + skip: 0 +}) +``` + + + + +`POST /api/v1/records/search` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "where": {"rating": {"$gte": 8}, "genre": "sci-fi"}, + "orderBy": {"rating": "desc"}, + "limit": 20, + "skip": 0 + }' +``` + + + + +### SearchQuery parameters + +| Field | Type | Description | +| --------- | ------------------ | ------------------------------------------------------------------------------------ | +| `labels` | `string[]` | Filter by one or more labels | +| `where` | `object` | Field conditions and operators (see [Where Operators](/reference/where-operators)) | +| `orderBy` | `string \| object` | Sort criteria (see [Pagination & Order](/reference/pagination-order)) | +| `limit` | `number` | Max records to return (default: 1000). **Omit when using `select`** | +| `skip` | `number` | Records to skip for pagination | +| `select` | `object` | Output-shaping expressions (see [Select Expressions](/reference/select-expressions)) | +| `groupBy` | `string[]` | Grouping keys, e.g. `['$record.genre']` | + +--- + +## Where Operators + +```python title="Python" +# Numeric comparisons +{"rating": {"$gt": 7, "$lt": 10}} +{"rating": {"$gte": 8, "$lte": 9.5}} + +# Set membership +{"genre": {"$in": ["sci-fi", "thriller"]}} +{"genre": {"$nin": ["romance"]}} + +# Text matching +{"title": {"$contains": "dark"}} +{"title": {"$startsWith": "The"}} +{"title": {"$endsWith": "Returns"}} + +# Existence and type checks +{"poster": {"$exists": True}} +{"rating": {"$type": "number"}} + +# Logical operators +{"$and": [{"genre": "sci-fi"}, {"rating": {"$gte": 8}}]} +{"$or": [{"genre": "sci-fi"}, {"rating": {"$gte": 9}}]} +{"$not": {"genre": "romance"}} +``` + +Full reference: [Where Operators](/reference/where-operators) + +--- + +## Relationship Traversal + +Filter across graph edges inline with `where`: + + + + +```python +# Movies that have an ACTOR from the USA +result = db.records.find({ + "labels": ["MOVIE"], + "where": { + "ACTOR": {"country": "USA"} + } +}) + +# With explicit relationship type and direction +result = db.records.find({ + "labels": ["MOVIE"], + "where": { + "ACTOR": { + "$relation": {"type": "STARS_IN", "direction": "in"}, + "country": "USA" + } + } +}) +``` + + + + +```typescript +// Movies that have an ACTOR from the USA +const { data } = await db.records.find({ + labels: ['MOVIE'], + where: { + ACTOR: { country: 'USA' } + } +}) + +// With explicit relationship type and direction +const { data: films } = await db.records.find({ + labels: ['MOVIE'], + where: { + DIRECTOR: { + $relation: { type: 'DIRECTED_BY', direction: 'out' }, + name: { $contains: 'Nolan' } + } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "where": { + "ACTOR": { + "$relation": {"type": "STARS_IN", "direction": "in"}, + "country": "USA" + } + } + }' +``` + + + + +--- + +## Select Expressions & Aggregations + +:::danger Do not set `limit` when using `select` +It restricts the record scan and produces mathematically incorrect totals. Use `orderBy` on a `select` output key instead. +::: + + + + +```python +result = db.records.find({ + "labels": ["MOVIE"], + "where": {"ACTOR": {"$alias": "$actor", "country": "USA"}}, + "select": { + "title": "$record.title", + "actorCount": {"$count": "$actor"}, + "avgRating": {"$avg": "$record.rating", "$precision": 1}, + "actorNames": {"$collect": { + "from": "$actor", + "select": {"name": "$actor.name"} + }} + } +}) +``` + +Available expressions: `$count` · `$sum` · `$avg` · `$min` · `$max` · `$collect` · `$timeBucket` · `$ref` · `$add` · `$subtract` · `$multiply` · `$divide` + + + + +```typescript +const stats = await db.records.find({ + labels: ['MOVIE'], + where: { ACTOR: { $alias: '$actor', country: 'USA' } }, + select: { + title: '$record.title', + actorCount: { $count: '$actor' }, + avgRating: { $avg: '$record.rating', $precision: 1 }, + actorNames: { $collect: { from: '$actor', select: { name: '$actor.name' } } } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "select": { + "count": {"$count": "*"}, + "avgRating": {"$avg": "$record.rating"} + } + }' +``` + + + + +Full reference: [Select Expressions](/reference/select-expressions) + +--- + +## Group By + + + + +```python +result = db.records.find({ + "labels": ["MOVIE"], + "select": { + "count": {"$count": "*"}, + "avgRating": {"$avg": "$record.rating"} + }, + "groupBy": ["$record.genre"], + "orderBy": {"count": "desc"} +}) +# [{"genre": "sci-fi", "count": 42, "avgRating": 7.9}, ...] +``` + + + + +```typescript +const byGenre = await db.records.find({ + labels: ['MOVIE'], + select: { + count: { $count: '*' }, + avgRating: { $avg: '$record.rating', $precision: 1 } + }, + groupBy: ['$record.genre'], + orderBy: { count: 'desc' } +}) +// [{ genre: 'sci-fi', count: 42, avgRating: 7.9 }, ...] +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "select": { + "count": {"$count": "*"}, + "avgRating": {"$avg": "$record.rating"} + }, + "groupBy": ["$record.genre"], + "orderBy": {"count": "desc"} + }' +``` + + + + +Full reference: [Group By](/reference/group-by) + +--- + +## TimeBucket (Time-Series) + + + + +```python +result = db.records.find({ + "labels": ["ORDER"], + "select": { + "day": {"$timeBucket": {"field": "$record.createdAt", "unit": "day"}}, + "count": {"$count": "*"} + }, + "groupBy": ["day"], + "orderBy": {"day": "asc"} +}) +``` + + + + +```typescript +const daily = await db.records.find({ + labels: ['ORDER'], + select: { + day: { $timeBucket: { field: '$record.createdAt', unit: 'day' } }, + count: { $count: '*' } + }, + groupBy: ['day'], + orderBy: { day: 'asc' } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["ORDER"], + "select": { + "day": {"$timeBucket": {"field": "$record.createdAt", "unit": "day"}}, + "count": {"$count": "*"} + }, + "groupBy": ["day"], + "orderBy": {"day": "asc"} + }' +``` + + + + +`unit` values: `day` · `week` · `month` · `quarter` · `year` · `hours` · `minutes` · `seconds` (use plural + `size` for custom window widths). + +--- + +## Collect Related Records + +Pull related records inline with the query result. + +**Label-based** (preferred for nesting — no alias required): + + + + +```python +result = db.records.find({ + "labels": ["COMPANY"], + "select": { + "name": "$record.name", + "departments": { + "$collect": { + "label": "DEPARTMENT", + "select": { + "name": "$self.name", + "projects": { + "$collect": { + "label": "PROJECT", + "select": {"name": "$self.name"} + } + } + } + } + } + } +}) +``` + + + + +```typescript +const companies = await db.records.find({ + labels: ['COMPANY'], + select: { + name: '$record.name', + departments: { + $collect: { + label: 'DEPARTMENT', + select: { + name: '$self.name', + projects: { + $collect: { + label: 'PROJECT', + select: { name: '$self.name' } + } + } + } + } + } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["COMPANY"], + "select": { + "name": "$record.name", + "departments": { + "$collect": { + "label": "DEPARTMENT", + "select": {"name": "$self.name"} + } + } + } + }' +``` + + + + +**Alias-based** (requires `$alias` in `where`): + + + + +```python +result = db.records.find({ + "labels": ["MOVIE"], + "where": {"ACTOR": {"$alias": "$actor"}}, + "select": { + "title": "$record.title", + "actors": { + "$collect": { + "from": "$actor", + "select": {"name": "$actor.name", "country": "$actor.country"} + } + } + } +}) +``` + + + + +```typescript +const movies = await db.records.find({ + labels: ['MOVIE'], + where: { ACTOR: { $alias: '$actor' } }, + select: { + title: '$record.title', + actors: { + $collect: { + from: '$actor', + select: { name: '$actor.name', country: '$actor.country' } + } + } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "where": {"ACTOR": {"$alias": "$actor"}}, + "select": { + "title": "$record.title", + "actors": { + "$collect": { + "from": "$actor", + "select": {"name": "$actor.name", "country": "$actor.country"} + } + } + } + }' +``` + + + + +--- + +## SearchResult + + + + +```python +result = db.records.find({"labels": ["MOVIE"], "limit": 10}) + +len(result) # records returned in this page (≤ limit) +result.total # total records matching in the database +result.has_more # True if more pages remain +result[0] # access by index +for r in result: # iterable + pass +``` + + + + +```typescript +const { data, total } = await db.records.find({ labels: ['MOVIE'], limit: 10 }) +// data: DBRecordInstance[] +// total: number — total records matching in the database +``` + + + + +```json +{ + "data": [...], + "total": 42 +} +``` + + + + +--- + +## Contextual Search (REST) + +Search within a specific record's relationships: + +```bash +POST /api/v1/records/:entityId/search +``` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/movie-123/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"labels": ["ACTOR"], "where": {"country": "USA"}}' +``` + +--- + +## In a Transaction + + + + +```python +tx = db.tx.begin() +try: + result = db.records.find({"labels": ["MOVIE"]}, transaction=tx) + tx.commit() +except Exception: + tx.rollback() + raise +``` + + + + +```typescript +const tx = await db.tx.begin() +try { + const { data } = await db.records.find({ labels: ['MOVIE'] }, tx) + await tx.commit() +} catch (e) { + await tx.rollback() + throw e +} +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "X-Transaction-Id: $TX_ID" \ + -d '{"labels": ["MOVIE"]}' +``` + + + + +--- + +## TypeScript: Via Model + +```typescript +const MovieModel = new Model('MOVIE', { + title: { type: 'string' }, + rating: { type: 'number' } +}) + +const all = await MovieModel.find() +const sciFi = await MovieModel.find({ where: { genre: 'sci-fi' } }) +const one = await MovieModel.findOne({ where: { title: 'Inception' } }) +const byId = await MovieModel.findById('movie-id-123') +const unique = await MovieModel.findUniq({ where: { title: 'Inception' } }) +``` + +Model search methods auto-fill `labels` from the model definition. + +--- + +## See also + +- [Where Operators](/reference/where-operators) — full operator reference +- [Select Expressions](/reference/select-expressions) — aggregation & output shaping +- [Group By](/reference/group-by) — grouping reference +- [Pagination & Order](/reference/pagination-order) — sorting and paging +- [Connect Records](/build/graph/connect-records) — traverse and filter relationships +- [Semantic Search](/build/ai-search/semantic-search) — vector + filter search diff --git a/docs/docs/learn/records-and-queries/import-data.mdx b/docs/docs/learn/records-and-queries/import-data.mdx new file mode 100644 index 00000000..d9a89115 --- /dev/null +++ b/docs/docs/learn/records-and-queries/import-data.mdx @@ -0,0 +1,394 @@ +--- +slug: /build/data/import-data +sidebar_position: 2 +title: Import Data +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Import Data + +RushDB accepts raw data — JSON objects, nested trees, flat arrays, or CSV — and turns it into a fully typed, linked graph. No schema definitions, no migrations, no manual relationship wiring. + +## How Nested Data Becomes a Graph + +When you import a nested JSON object, RushDB walks the structure with a breadth-first search (BFS) algorithm. Each nested object becomes a separate record, linked to its parent by a relationship. + +```json +{ + "title": "Inception", + "rating": 8.8, + "ACTOR": [ + { "name": "Leonardo DiCaprio", "country": "USA" }, + { "name": "Ken Watanabe", "country": "Japan" } + ] +} +``` + +This single call produces **3 records** (`MOVIE` + `ACTOR` × 2) with relationships between them, plus typed properties on each — all inferred automatically. + +```mermaid +graph LR + MOVIE["MOVIE
title: Inception · rating: 8.8"] + A1["ACTOR
name: Leonardo DiCaprio"] + A2["ACTOR
name: Ken Watanabe"] + MOVIE -->|"auto-linked"| A1 + MOVIE -->|"auto-linked"| A2 +``` + +### The ingestion pipeline + +1. **Parse** — BFS walk. Each nested object becomes a separate record. +2. **Type inference** — Every value is classified as `string`, `number`, `boolean`, `datetime`, or `null`. +3. **Label assignment** — Top-level records use the label you provide. Nested objects derive their label from the parent key name (e.g., key `"engine"` → label `Engine`). +4. **Relationship creation** — Parent → child records are linked with default relationships (`__RUSHDB__RELATION__DEFAULT__`). + +--- + +## Import Nested JSON + + + + +`db.records.create_many()` — pass a dict with nested structure. + +```python +db.records.create_many( + label="MOVIE", + data={ + "title": "Inception", + "rating": 8.8, + "ACTOR": [ + {"name": "Leonardo DiCaprio", "country": "USA"}, + {"name": "Ken Watanabe", "country": "Japan"} + ] + } +) +# MOVIE → ACTOR × 2: all created and linked automatically +``` + +Infer the label from a single top-level key: + +```python +db.records.create_many( + data={"ITEM": [ + {"name": "Sprocket", "weight": 1.2}, + {"name": "Cog", "weight": 0.7} + ]} +) +# label inferred as 'ITEM' +``` + + + + +`db.records.importJson()` + +```typescript +const imported = await db.records.importJson({ + label: 'MOVIE', + data: { + title: 'Inception', + rating: 8.8, + ACTOR: [ + { name: 'Leonardo DiCaprio', country: 'USA' }, + { name: 'Ken Watanabe', country: 'Japan' } + ] + }, + options: { suggestTypes: true } +}) +``` + +Infer the label from a single top-level key: + +```typescript +await db.records.importJson({ + data: { + ITEM: [ + { name: 'Sprocket', weight: 1.2 }, + { name: 'Cog', weight: 0.7 } + ] + } +}) +// label inferred as 'ITEM' +``` + +:::warning Invalid root object +If you omit `label` and the top level is not a single-key map, `importJson` throws: +`importJson requires either an explicit label or a single top-level key to infer the label` +::: + + + + +`POST /api/v1/records/import/json` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "MOVIE", + "data": { + "title": "Inception", + "rating": 8.8, + "ACTOR": [ + {"name": "Leonardo DiCaprio", "country": "USA"}, + {"name": "Ken Watanabe", "country": "Japan"} + ] + }, + "options": {"suggestTypes": true} + }' +``` + + + + +--- + +## Import Flat Arrays + +Use this for flat, row-like data (no nested objects inside items). This is the fastest path for CSV-shaped data. + + + + +`db.records.create_many()` — pass a list. + +```python +db.records.create_many( + label="ACTOR", + data=[ + {"name": "Leonardo DiCaprio", "country": "USA"}, + {"name": "Ken Watanabe", "country": "Japan"} + ], + options={"suggestTypes": True} +) +``` + + + + +`db.records.createMany()` + +```typescript +await db.records.createMany({ + label: 'ACTOR', + data: [ + { name: 'Leonardo DiCaprio', country: 'USA' }, + { name: 'Ken Watanabe', country: 'Japan' } + ], + options: { suggestTypes: true } +}) +``` + + + + +`POST /api/v1/records/import/json` — pass an array as `data`. + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "ACTOR", + "data": [ + {"name": "Leonardo DiCaprio", "country": "USA"}, + {"name": "Ken Watanabe", "country": "Japan"} + ], + "options": {"suggestTypes": true} + }' +``` + + + + +--- + +## Import CSV + + + + +`db.records.import_csv()` + +```python +with open("actors.csv") as f: + csv_content = f.read() + +db.records.import_csv( + label="ACTOR", + data=csv_content, + options={"suggestTypes": True, "returnResult": False}, + parse_config={"header": True, "dynamicTyping": True} +) +``` + + + + +`db.records.importCsv()` + +```typescript +const csv = `name,email,age\nJohn Doe,john@example.com,30\nJane Smith,jane@example.com,25` + +const result = await db.records.importCsv({ + label: 'CUSTOMER', + data: csv, + options: { + suggestTypes: true, + convertNumericValuesToNumbers: true, + returnResult: true + }, + parseConfig: { + delimiter: ',', + header: true, + skipEmptyLines: true, + dynamicTyping: true + } +}) +``` + + + + +`POST /api/v1/records/import/csv` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/csv \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "ACTOR", + "data": "name,country\nLeonardo DiCaprio,USA\nKen Watanabe,Japan", + "options": {"suggestTypes": true}, + "parseConfig": {"header": true, "dynamicTyping": true} + }' +``` + + + + +### CSV `parseConfig` options + +| Option | Default | Description | +| ---------------- | ---------------------------- | --------------------------------------------------------------- | +| `delimiter` | `,` | Column separator | +| `header` | `true` | First row is header | +| `skipEmptyLines` | `true` | Ignore blank rows (`"greedy"` also skips whitespace-only lines) | +| `dynamicTyping` | inherits from `suggestTypes` | Auto-convert numbers and booleans | +| `quoteChar` | `"` | Quote character | +| `escapeChar` | `"` | Escape character | +| `newline` | auto | Explicit newline sequence override | + +--- + +## Upsert during Import + +All import methods support upsert via `mergeBy` and `mergeStrategy`. + + + + +```python +# Append — update matched records, preserve other fields +db.records.create_many( + label="ACTOR", + data=actors, + options={"mergeBy": ["name"], "mergeStrategy": "append"} +) + +# Rewrite — replace all properties for matched records +db.records.import_csv( + label="ACTOR", + data=csv_content, + options={"mergeBy": ["name"], "mergeStrategy": "rewrite"} +) +``` + + + + +```typescript +// Upsert (append) by email +await db.records.createMany({ + label: 'AUTHOR', + data: authors, + options: { mergeBy: ['email'], mergeStrategy: 'append', suggestTypes: true } +}) + +// Rewrite (replace) by email +await db.records.importJson({ + label: 'AUTHOR', + data: authors, + options: { mergeBy: ['email'], mergeStrategy: 'rewrite' } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "ACTOR", + "data": [ + {"name": "Leonardo DiCaprio", "country": "USA"}, + {"name": "Ken Watanabe", "country": "Japan"} + ], + "options": {"mergeBy": ["name"], "mergeStrategy": "append"} + }' +``` + + + + +### Merge strategies + +| Strategy | Behaviour | +| ------------------ | --------------------------------------------------------------------- | +| `append` (default) | Add / update incoming fields; preserve all other existing fields | +| `rewrite` | Replace all fields with incoming data; unmentioned fields are removed | + +### `mergeBy` behaviour + +| `mergeBy` value | Match behaviour | +| --------------- | ----------------------------------- | +| `["field"]` | Match only on listed fields | +| `[]` or omitted | Match on ALL incoming property keys | + +--- + +## Import Options + +| Option | Type | Default | Description | +| ------------------------------- | ---------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `suggestTypes` | `boolean` | `true` | Infer property types automatically. Set to `false` to store all values as strings. | +| `convertNumericValuesToNumbers` | `boolean` | `false` | Convert string numbers to number type | +| `capitalizeLabels` | `boolean` | `false` | Uppercase all auto-derived label names | +| `relationshipType` | `string` | `__RUSHDB__RELATION__DEFAULT__` | Relationship type for nested links | +| `returnResult` | `boolean` | `false` | Return created records in the response. Ignored for imports >1 000 records (summary returned instead). | +| `mergeBy` | `string[]` | `undefined` | Property names to match existing records on. If omitted with `mergeStrategy` present, all incoming keys are used. | +| `mergeStrategy` | `string` | `append` | `append` or `rewrite`. Providing either option triggers upsert semantics. | + +--- + +## Method Quick Reference + +| Scenario | Python | TypeScript | REST | +| ----------- | ------------------------------ | ------------------------------- | ------------------------------- | +| Flat rows | `create_many(label, data=[…])` | `createMany({label, data:[…]})` | `POST /import/json` with array | +| Nested JSON | `create_many(label, data={…})` | `importJson({label, data:{…}})` | `POST /import/json` with object | +| CSV string | `import_csv(label, data=csv)` | `importCsv({label, data:csv})` | `POST /import/csv` | + +--- + +## See also + +- [Store Records](/build/data/store-records) — flat record create / update / delete operations +- [Connect Records](/build/graph/connect-records) — manually attach/detach relationships +- [Write Records with Vectors](/build/ai-search/write-with-vectors) — attach embedding vectors at import time diff --git a/docs/docs/learn/records-and-queries/index.mdx b/docs/docs/learn/records-and-queries/index.mdx new file mode 100644 index 00000000..3f5fa787 --- /dev/null +++ b/docs/docs/learn/records-and-queries/index.mdx @@ -0,0 +1,44 @@ +--- +slug: /build/data/ +sidebar_position: 0 +title: Records & Queries +--- + +# Records & Queries + +RushDB stores data as **Records**: labeled graph nodes with typed properties. You can write flat rows, import nested JSON, inspect the schema that emerges, and query records through one composable `SearchQuery` structure. + +There is no schema migration step before the first write. RushDB infers property types, keeps schema metadata in the graph, and exposes the live result through labels, properties, and ontology APIs. + +## Start Here + +| Goal | Guide | +| ---------------------------------------------------------- | ------------------------------------------ | +| Create, update, or upsert records | [Store Records](/build/data/store-records) | +| Import nested JSON and create linked records automatically | [Import Data](/build/data/import-data) | +| Filter, sort, paginate, and traverse records | [Find & Query](/build/data/find-and-query) | +| Export matching records as CSV | [Export Data](/build/data/export-data) | + +## Understand the Adaptive Schema + +| Goal | Guide | +| ------------------------------------------------------- | ---------------------------------------------------------------------- | +| Inspect labels, property definitions, types, and values | [Labels & Properties](/build/schema/labels-and-properties) | +| Discover the full live schema in one call | [Discover Your Schema](/build/schema/discover-your-schema) | +| Understand the internal graph architecture | [Labeled Meta Property Graph](/build/data/labeled-meta-property-graph) | + +RushDB properties are more than field names. Internally, each `(name, type)` pair is represented as a metadata node connected to the records that carry it. That graph-backed property layer is what makes runtime discovery, flexible ingestion, and ontology generation possible without predefined tables. + +## Advanced Operations + +| Goal | Guide | +| ---------------------------------------------- | ------------------------------------------------ | +| Run raw Cypher against supported deployments | [Raw Queries](/build/raw-queries) | +| Group multiple writes into an atomic operation | [Transactions](/build/reliability/transactions) | +| Read the full query language reference | [SearchQuery Reference](/reference/search-query) | + +## Related Sections + +- [Relationships](/build/graph/) — connect records and traverse graph edges +- [Semantic Search](/build/ai-search/semantic-search) — search indexed text properties by meaning +- [Main Concepts](/build/data/labeled-meta-property-graph) — broader mental model for RushDB diff --git a/docs/docs/learn/records-and-queries/labeled-meta-property-graph.mdx b/docs/docs/learn/records-and-queries/labeled-meta-property-graph.mdx new file mode 100644 index 00000000..0f893b9b --- /dev/null +++ b/docs/docs/learn/records-and-queries/labeled-meta-property-graph.mdx @@ -0,0 +1,172 @@ +--- +slug: /build/data/labeled-meta-property-graph +sidebar_position: 7 +title: Labeled Meta Property Graph +--- + +# Labeled Meta Property Graph + +RushDB uses a **Labeled Meta Property Graph** model: application records remain flexible graph nodes, while their field definitions are represented as metadata nodes inside the same graph. + +This page explains the internal model. You do not need to understand these implementation details to store or query data, but they explain why RushDB can accept evolving JSON, expose its live schema, and help agents reason over unknown datasets. + +## The Short Version + +When you write: + +```json +{ + "label": "PRODUCT", + "data": { + "name": "Trail Jacket", + "price": 129.99, + "inStock": true + } +} +``` + +RushDB stores one `PRODUCT` record with the values. As part of that same write path, it ensures schema metadata nodes exist for: + +```text +(name, string) +(price, number) +(inStock, boolean) +``` + +Each successful write extends an emergent set of schema guardrails: metadata nodes are connected to records that carry the corresponding field and inferred type. + +```mermaid +graph LR + P1["PROPERTY
name · string"] + P2["PROPERTY
price · number"] + P3["PROPERTY
inStock · boolean"] + R["RECORD · PRODUCT
name: Trail Jacket
price: 129.99
inStock: true"] + + P1 -->|"schema guardrail"| R + P2 -->|"schema guardrail"| R + P3 -->|"schema guardrail"| R +``` + +The values stay on the record. Property nodes store schema metadata, not duplicated application data. Internally, RushDB represents each guardrail with a `__RUSHDB__RELATION__VALUE__` edge. Despite its internal name, that edge indicates that a record carries a property definition; it is not the value's storage location. + +## Why Property Nodes Exist + +Most databases keep schema outside the data: + +- relational databases define columns in table metadata; +- document stores often leave field discovery to application code or sampling; +- conventional property graphs store values directly on nodes but do not necessarily maintain a queryable field catalog. + +RushDB keeps a compact, queryable metadata layer inside the graph. This provides: + +| Capability | Why the metadata layer helps | +| ------------------------ | -------------------------------------------------------------------------------- | +| Runtime schema discovery | RushDB can enumerate the fields that actually exist | +| Type-aware querying | Agents and applications can inspect types before building filters | +| Value exploration | Property IDs provide a stable handle for distinct values and numeric/date ranges | +| Flexible ingestion | New fields appear on write without a migration | +| Schema evolution | Old and new record shapes can coexist while the live ontology reflects both | +| Semantic indexing | Embedding policies attach to a label and property name discovered from the graph | + +## Records, Labels, Properties, Relationships + +| Layer | Stores | Example | +| ------------- | ----------------------------------------------- | ---------------------------------- | +| Record | Application values and system metadata | `PRODUCT { name, price, inStock }` | +| Label | The record's entity type | `PRODUCT`, `ORDER`, `SESSION` | +| Property node | A field definition identified by `(name, type)` | `(price, number)` | +| Relationship | A directed edge between graph nodes | `ORDER -[CONTAINS]-> PRODUCT` | + +Records remain the source of truth for values. The property layer describes the shape of those records. + +## Identity Is Name Plus Type + +Property identity includes both the field name and its inferred type. If different records send: + +```json +[ + { "label": "PRODUCT", "data": { "size": "M" } }, + { "label": "PRODUCT", "data": { "size": 42 } } +] +``` + +RushDB can represent both definitions: + +```text +(size, string) +(size, number) +``` + +This preserves information instead of forcing unrelated values into one rigid column. It also gives you a signal that upstream data may need normalization. + +For predictable queries, keep a property's type consistent within an entity model whenever possible. + +## How the Schema Emerges + +Every write passes through the same conceptual pipeline: + +1. **Accept JSON** — receive flat or nested input. +2. **Infer types** — classify scalar and array values. +3. **Create records** — store application values on record nodes. +4. **Upsert property metadata** — ensure each `(name, type)` definition exists. +5. **Attach schema guardrails** — connect property nodes to records carrying those fields and inferred types. +6. **Decompose nested objects** — create child records and parent-child relationships where needed. + +No schema declaration is required before step 1. The schema emerges from successful writes. + +## From Metadata Graph to Ontology + +The ontology APIs turn the internal graph into an efficient runtime schema snapshot: + +```text +getOntologyMarkdown() + → labels and record counts + → properties per label + → inferred types + → numeric and datetime ranges + → sample string and boolean values + → relationship directions + → semantic index availability +``` + +Agents should call `getOntologyMarkdown` before constructing queries. Applications that need structured metadata for filters or admin interfaces can call `getOntology`. + +For deeper inspection: + +| API | Use | +| ------------------- | --------------------------------------------------------------- | +| `findProperties` | List property definitions and their IDs | +| `propertyValues` | Enumerate string/boolean values or retrieve numeric/date ranges | +| `findLabels` | List labels and record counts | +| `findRelationships` | Explore edges between record types | + +## Adaptive Does Not Mean Uncontrolled + +RushDB does not require upfront migrations, but production data still benefits from conventions: + +- use stable labels for entity types; +- use consistent property names and types; +- use ISO 8601 timestamps; +- model state with fields such as `status`, not new labels; +- inspect ontology output during ingestion development; +- normalize accidental type drift at the source when possible. + +The goal is not to eliminate structure. The goal is to let structure emerge from real data, remain inspectable, and evolve without blocking writes. + +## Tradeoffs + +The metadata layer is useful because it is explicit, but it is not free: + +| Tradeoff | Practical implication | +| ------------------------------------------- | ----------------------------------------------------------------------------------------- | +| Type drift remains visible | Mixed `(name, type)` definitions can appear until data is normalized | +| Flexible records require disciplined naming | `userId` and `userID` are distinct fields | +| Discovery is runtime state | Ontology reflects what has been written, not an aspirational schema file | +| Internal metadata should stay internal | Query application records through RushDB APIs rather than editing metadata nodes directly | + +## Next Steps + +- [Labels & Properties](/build/schema/labels-and-properties) — inspect and manage schema metadata through public APIs +- [Discover Your Schema](/build/schema/discover-your-schema) — retrieve the live ontology +- [Import Data](/build/data/import-data) — create records and relationships from nested JSON +- [Relationships](/build/graph/) — understand explicit and generated graph edges diff --git a/docs/docs/learn/records-and-queries/labels-and-properties.mdx b/docs/docs/learn/records-and-queries/labels-and-properties.mdx new file mode 100644 index 00000000..4b2040a3 --- /dev/null +++ b/docs/docs/learn/records-and-queries/labels-and-properties.mdx @@ -0,0 +1,272 @@ +--- +slug: /build/schema/labels-and-properties +sidebar_position: 1 +title: Labels & Properties +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Labels & Properties + +RushDB is schema-free — labels and properties emerge automatically as you write records. These APIs let you inspect, enumerate, and manage your schema at runtime. + +--- + +## Labels + +A **label** is a string tag applied to every record (e.g. `"Movie"`, `"User"`, `"Order"`). The labels API lets you discover which labels exist and how many records each has. + +### Find Labels + + + + +`db.labels.find()` + +```python +# All labels and their record counts +result = db.labels.find({}) +# → [LabelResult(name='MOVIE', count=3), LabelResult(name='ACTOR', count=3), ...] + +# Labels that have records matching a condition +result = db.labels.find({"where": {"rating": {"$gte": 8}}}) +# → [LabelResult(name='MOVIE', count=1)] +``` + +`db.labels.find()` accepts a standard [`where`](/reference/where-operators) clause. It returns all labels that have at least one record matching the condition. + + + + +`db.labels.find()` + +```typescript +// All labels +const { data } = await db.labels.find() +// { MOVIE: 84, ACTOR: 312, DIRECTOR: 47 } + +// Labels for records where rating > 8 +const { data } = await db.labels.find({ + where: { rating: { $gt: 8 } } +}) +// { MOVIE: 21 } +``` + +`find()` accepts the same `where`, `skip`, `limit` parameters as `db.records.find()`. + + + + +`POST /api/v1/labels/search` + +```bash +# All labels +curl -X POST https://api.rushdb.com/api/v1/labels/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{}' + +# Labels that have records matching a condition +curl -X POST https://api.rushdb.com/api/v1/labels/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"where": {"rating": {"$gte": 8}}}' +``` + +Response: `{ [label]: count }` map. + + + + +--- + +## Properties + +A **property** is a field definition created automatically when you write a record with a new key. The properties API lets you inspect types, enumerate distinct values, and delete fields. + +### Find Properties + + + + +`db.properties.find()` + +```python +# All properties +props = db.properties.find() + +# Filtered by type +props = db.properties.find({ + "where": {"type": "string"}, + "limit": 20 +}) +``` + + + + +`db.properties.find()` + +```typescript +const { data } = await db.properties.find({ + where: { type: 'number' } +}) +// [{ id, name: 'rating', type: 'number', ... }, ...] +``` + + + + +`POST /api/v1/properties/search` + +```bash +curl -X POST https://api.rushdb.com/api/v1/properties/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"where": {"type": "string"}}' +``` + + + + +### Find by ID + + + + +`db.properties.find_by_id(property_id)` + +```python +prop = db.properties.find_by_id("prop-123") +``` + + + + +`db.properties.findById(id)` + +```typescript +const prop = await db.properties.findById('property-id') +``` + + + + +`GET /api/v1/properties/:propertyId` + +```bash +curl https://api.rushdb.com/api/v1/properties/prop-123 \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +### Get Property Values + +Enumerate distinct values for a property — useful for building filter UIs, autocomplete, or feeding into `db.ai.get_ontology()`. + + + + +`db.properties.values(property_id, search_query?)` + +```python +values_data = db.properties.values( + property_id="prop-123", + search_query={ + "query": "sci", # filter values containing this text + "orderBy": "asc", + "limit": 100 + } +) + +print(values_data.get("values")) # list of distinct values +print(values_data.get("min")) # numeric min (number/datetime props) +print(values_data.get("max")) # numeric max (number/datetime props) +``` + + + + +`db.properties.values(propertyId, params?)` + +```typescript +const { data: genres } = await db.properties.values('prop-id-genre') +// ['sci-fi', 'action', 'drama', ...] + +// With filter +const { data } = await db.properties.values('prop-id', { + query: 'sci', // text prefix filter + orderBy: 'asc', + limit: 10 +}) +``` + + + + +`POST /api/v1/properties/:propertyId/values` + +```bash +curl -X POST https://api.rushdb.com/api/v1/properties/prop-123/values \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"query": "sci", "orderBy": "asc", "limit": 100}' +``` + +| Field | Type | Description | +| --------- | ----------------- | ---------------------------------- | +| `query` | `string` | Filter values containing this text | +| `orderBy` | `"asc" \| "desc"` | Sort direction | +| `skip` | `number` | Pagination offset | +| `limit` | `number` | Max values to return | + + + + +### Delete Property + +:::warning +Deletes the property definition and removes it from **all records** that have it. This is irreversible. +::: + + + + +`db.properties.delete(property_id)` + +```python +db.properties.delete("prop-123") +``` + + + + +`db.properties.delete(id)` + +```typescript +await db.properties.delete('property-id') +``` + + + + +`DELETE /api/v1/properties/:propertyId` + +```bash +curl -X DELETE https://api.rushdb.com/api/v1/properties/prop-123 \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +--- + +## See also + +- [Discover Your Schema](/build/schema/discover-your-schema) — live ontology via `db.ai.getOntology()` +- [Find & Query](/build/data/find-and-query) — query records by property value +- [Where Operators](/reference/where-operators) — filter syntax reference diff --git a/docs/docs/learn/records-and-queries/raw-queries.mdx b/docs/docs/learn/records-and-queries/raw-queries.mdx new file mode 100644 index 00000000..1700f1d2 --- /dev/null +++ b/docs/docs/learn/records-and-queries/raw-queries.mdx @@ -0,0 +1,138 @@ +--- +slug: /build/raw-queries +sidebar_position: 2 +title: Raw Queries +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Raw Queries + +:::warning Requires a connected Neo4j instance +This endpoint is only available when your project is connected to your own Neo4j database. Connecting a custom Neo4j instance is available on the free tier — see the RushDB dashboard to set it up. +::: + +Run arbitrary Cypher queries against your connected Neo4j database. This is intended for advanced use-cases where the SearchQuery API is not expressive enough. + +--- + +## Basic Usage + + + + +`db.query.raw()` + +```python +from rushdb import RushDB + +db = RushDB("RUSHDB_API_KEY") + +result = db.query.raw({ + "query": "MATCH (n:Person) RETURN n LIMIT $limit", + "params": {"limit": 10} +}) + +print(result.get("data")) +``` + + + + +`db.query.raw()` + +```typescript +const result = await db.query.raw({ + query: 'MATCH (n:Person) RETURN n LIMIT $limit', + params: { limit: 10 } +}) + +// `result` contains the server response with query records as returned by Neo4j driver +console.log(result) +``` + + + + +`POST /api/v1/query/raw` + +```bash +curl -X POST https://api.rushdb.com/api/v1/query/raw \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "query": "MATCH (n:Person) RETURN n LIMIT $limit", + "params": { "limit": 10 } + }' +``` + +Response: raw Neo4j driver result object. + + + + +--- + +## Real-World Example + +Find people employed by a company and return selected fields: + + + + +```python +company = "Acme Corp" +result = db.query.raw({ + "query": """ + MATCH (c:Company { name: $company })<-[:EMPLOYS]-(p:Person) + RETURN p { .name, .email, company: c.name } AS employee + ORDER BY p.name + LIMIT $limit + """, + "params": {"company": company, "limit": 50} +}) + +print(result.get("data")) +``` + + + + +```typescript +const company = 'Acme Corp' +const result = await db.query.raw({ + query: ` + MATCH (c:Company { name: $company })<-[:EMPLOYS]-(p:Person) + RETURN p { .name, .email, company: c.name } AS employee + ORDER BY p.name + LIMIT $limit + `, + params: { company, limit: 50 } +}) + +console.log(result.data) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/query/raw \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "query": "MATCH (c:Company { name: $company })<-[:EMPLOYS]-(p:Person) RETURN p { .name, .email, company: c.name } AS employee ORDER BY p.name LIMIT $limit", + "params": { "company": "Acme Corp", "limit": 50 } + }' +``` + + + + +--- + +## See also + +- [Find & Query](/build/data/find-and-query) — SearchQuery API (recommended for most use cases) +- [Connect Records](/build/graph/connect-records) — relationship traversal via SearchQuery diff --git a/docs/docs/learn/records-and-queries/store-records.mdx b/docs/docs/learn/records-and-queries/store-records.mdx new file mode 100644 index 00000000..c939c184 --- /dev/null +++ b/docs/docs/learn/records-and-queries/store-records.mdx @@ -0,0 +1,603 @@ +--- +slug: /build/data/store-records +sidebar_position: 1 +title: Store Records +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Store Records + +A **Record** is a typed key-value node with a label. Records are the fundamental unit of data in RushDB — like a document in a document store, but with seamless relationship traversal built in. + +Each record has system properties (`__id`, `__label`, `__proptypes`) plus your own fields. + +--- + +## Create a Record + + + + +`db.records.create()` + +```python +movie = db.records.create( + label="MOVIE", + data={"title": "Inception", "rating": 8.8, "genre": "sci-fi"} +) +# → Record { __id, __label, title, rating, genre } +``` + + + + +`db.records.create()` + +```typescript +const movie = await db.records.create({ + label: 'MOVIE', + data: { title: 'Inception', rating: 8.8, genre: 'sci-fi' } +}) +// → DBRecordInstance { __id, __label, title, rating, genre } +``` + + + + +`POST /api/v1/records` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "MOVIE", + "data": {"title": "Inception", "rating": 8.8, "genre": "sci-fi"} + }' +``` + + + + +--- + +## Create Multiple Records + + + + +`db.records.create_many()` + +Flat rows only — no nested objects. For nested data use [`import_json`](/build/data/import-data). + +```python +result = db.records.create_many( + label="ACTOR", + data=[ + {"name": "Leonardo DiCaprio", "country": "USA"}, + {"name": "Ken Watanabe", "country": "Japan"} + ] +) +# → SearchResult { data: [...], total: 2 } +``` + + + + +`db.records.createMany()` + +Flat rows only — no nested objects. For nested data use [`importJson`](/build/data/import-data). + +```typescript +const result = await db.records.createMany({ + label: 'ACTOR', + data: [ + { name: 'Leonardo DiCaprio', country: 'USA' }, + { name: 'Ken Watanabe', country: 'Japan' } + ] +}) +// → DBRecordsArrayInstance { data: [...], total: 2 } +``` + + + + +`POST /api/v1/records/import/json` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "ACTOR", + "data": [ + {"name": "Leonardo DiCaprio", "country": "USA"}, + {"name": "Ken Watanabe", "country": "Japan"} + ] + }' +``` + + + + +--- + +## Upsert (Create or Update) + +Creates a new record if no match is found, or updates the existing record if matched. + + + + +`db.records.upsert()` + +```python +# Match on 'title'; update rating if found, create if not +movie = db.records.upsert( + label="MOVIE", + data={"title": "Inception", "rating": 9.0, "genre": "sci-fi"}, + options={"mergeBy": ["title"], "mergeStrategy": "append"} +) +``` + + + + +`db.records.upsert()` + +```typescript +const movie = await db.records.upsert({ + label: 'MOVIE', + data: { title: 'Inception', rating: 9.0, genre: 'sci-fi' }, + options: { mergeBy: ['title'], mergeStrategy: 'append' } +}) +``` + + + + +Supply `mergeBy` and/or `mergeStrategy` in `options` on the create endpoint to trigger upsert behavior. + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "MOVIE", + "data": {"title": "Inception", "rating": 9.0}, + "options": {"mergeBy": ["title"], "mergeStrategy": "append"} + }' +``` + + + + +### Merge strategies + +| Strategy | Behaviour | +| ------------------ | --------------------------------------------------------------------- | +| `append` (default) | Add / update incoming fields; preserve all other existing fields | +| `rewrite` | Replace all fields with incoming data; unmentioned fields are removed | + +### `mergeBy` behaviour + +| `mergeBy` value | Match behaviour | +| --------------- | ----------------------------------- | +| `['field']` | Match only on listed fields | +| `[]` or omitted | Match on ALL incoming property keys | + +--- + +## Partial Update + +Updates only the specified fields; all other fields are preserved. + + + + +`db.records.update()` + +```python +# Update via record object (recommended) +movie.update({"rating": 9.0}) + +# Also works: pass the Record directly or any RecordTarget +db.records.update(movie, {"rating": 9.0}) + +# Or by ID string +db.records.update("movie-123", {"rating": 9.0}) +``` + + + + +`db.records.update()` + +```typescript +await db.records.update({ + target: 'movie-id-123', + label: 'MOVIE', + data: { rating: 9.1 } +}) +// → DBRecordInstance (title, genre, etc. unchanged) +``` + + + + +`PATCH /api/v1/records/:entityId` + +```bash +curl -X PATCH https://api.rushdb.com/api/v1/records/movie-123 \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"data": {"rating": 9.0}}' +``` + + + + +--- + +## Full Replacement + +Replaces all fields. Fields not in `data` are removed. + + + + +`db.records.set()` + +```python +# Set via record object (recommended) +movie.set({"title": "Inception", "rating": 9.0, "genre": "sci-fi"}) + +# Also works: pass the Record directly or any RecordTarget +db.records.set(movie, {"title": "Inception", "rating": 9.0, "genre": "sci-fi"}) + +# Or by ID string +db.records.set("movie-123", {"title": "Inception", "rating": 9.0, "genre": "sci-fi"}) +``` + + + + +`db.records.set()` + +```typescript +await db.records.set({ + target: 'movie-id-123', + label: 'MOVIE', + data: { title: 'Inception', rating: 9.1, genre: 'sci-fi' } +}) +// → DBRecordInstance (only these three fields remain) +``` + + + + +`PUT /api/v1/records/:entityId` + +```bash +curl -X PUT https://api.rushdb.com/api/v1/records/movie-123 \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "MOVIE", + "data": {"title": "Inception", "rating": 9.0, "genre": "sci-fi"} + }' +``` + + + + +### update() vs set() parameters + +| Parameter | Type | Description | +| ------------- | --------------- | --------------------------------------------------------- | +| `target` | `RecordTarget` | UUID string, `Record` instance, or dict with `__id` | +| `label` | `str` | Label for the record (TypeScript only, required) | +| `data` | `dict / object` | Flat object or `PropertyDraft[]` for precise type control | +| `options` | `dict / object` | `suggestTypes`, `convertNumericValuesToNumbers` | +| `transaction` | `Transaction` | Optional transaction | + +--- + +## Delete Records + +### Delete by ID + + + + +`db.records.delete_by_id()` + +```python +# Single record +db.records.delete_by_id("movie-123") + +# Multiple records +db.records.delete_by_id(["movie-123", "movie-456"]) + +# From a record object +movie.delete() +``` + +All relationships attached to deleted records are removed automatically. + + + + +`db.records.deleteById()` + +```typescript +// Single record +await db.records.deleteById('movie-id-123') + +// Multiple records +await db.records.deleteById(['id-1', 'id-2', 'id-3']) +``` + +All relationships attached to deleted records are removed automatically. + + + + +`DELETE /api/v1/records/:entityId` + +```bash +curl -X DELETE https://api.rushdb.com/api/v1/records/movie-123 \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +### Bulk Delete + +Delete all records matching a query. + + + + +`db.records.delete()` + +```python +db.records.delete({ + "labels": ["MOVIE"], + "where": {"rating": {"$lt": 5}} +}) +``` + +:::warning +Calling `delete()` without a `where` clause deletes **all** records with the given label. +::: + + + + +`db.records.delete()` + +```typescript +await db.records.delete({ + labels: ['MOVIE'], + where: { genre: 'sci-fi', rating: { $lt: 5 } } +}) +``` + +:::warning +An empty `where` without `allowForceDelete: true` in the SDK config throws `EmptyTargetError`. +::: + + + + +`POST /api/v1/records/delete` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/delete \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"labels": ["MOVIE"], "where": {"rating": {"$lt": 5}}}' +``` + +:::warning +Omitting `where` deletes **all** records with the given label. +::: + + + + +--- + +## Options + +All write methods accept an `options` object: + +| Option | Default | Description | +| ------------------------------- | ------------------------------- | ----------------------------------------- | +| `suggestTypes` | `true` | Infer property types automatically | +| `convertNumericValuesToNumbers` | `false` | Convert string numbers to number type | +| `capitalizeLabels` | `false` | Uppercase all inferred label names | +| `relationshipType` | `__RUSHDB__RELATION__DEFAULT__` | Relationship type for nested links | +| `returnResult` | `false` | Return the created record in the response | +| `mergeBy` | — | Fields to match on for upsert | +| `mergeStrategy` | `append` | `append` or `rewrite` | + +--- + +## Precise Type Control + +By default, RushDB infers property types. Use `PropertyDraft` / `properties` array for explicit control. + + + + +```python +db.records.create( + label="MOVIE", + data=[ + {"name": "title", "type": "string", "value": "Inception"}, + {"name": "rating", "type": "number", "value": 8.8}, + {"name": "genres", "type": "string", "value": "sci-fi,thriller", "valueSeparator": ","}, + {"name": "releasedAt", "type": "datetime", "value": "2010-07-16T00:00:00Z"} + ] +) +``` + + + + +```typescript +await db.records.create({ + label: 'MOVIE', + data: [ + { name: 'title', type: 'string', value: 'Inception' }, + { name: 'rating', type: 'number', value: 8.8 }, + { name: 'genres', type: 'string', value: 'sci-fi,thriller', valueSeparator: ',' }, + { name: 'releasedAt', type: 'datetime', value: '2010-07-16T00:00:00Z' } + ] +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "MOVIE", + "properties": [ + {"name": "title", "type": "string", "value": "Inception"}, + {"name": "rating", "type": "number", "value": 8.8}, + {"name": "genres", "type": "string", "value": "sci-fi,thriller", "valueSeparator": ","}, + {"name": "releasedAt", "type": "datetime", "value": "2010-07-16T00:00:00Z"} + ] + }' +``` + + + + +### PropertyDraft fields + +| Field | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------- | +| `name` | `string` | Property name | +| `type` | `string` | `string` · `number` · `boolean` · `datetime` · `null` · `vector` | +| `value` | any | The value | +| `valueSeparator` | `string` | Split `value` string into an array on this separator | + +--- + +## In a Transaction + + + + +```python +tx = db.tx.begin() +try: + movie = db.records.create( + label="MOVIE", + data={"title": "Inception"}, + transaction=tx + ) + actor = db.records.create( + label="ACTOR", + data={"name": "Leonardo DiCaprio"}, + transaction=tx + ) + db.relationships.attach( + source=movie, + target=actor, + options={"type": "STARS"}, + transaction=tx + ) + tx.commit() +except Exception: + tx.rollback() + raise +``` + + + + +```typescript +const tx = await db.tx.begin() +try { + const movie = await db.records.create({ label: 'MOVIE', data: { title: 'Dune' } }, tx) + const actor = await db.records.create({ label: 'ACTOR', data: { name: 'Timothée Chalamet' } }, tx) + await db.records.attach({ source: movie, target: actor, options: { type: 'STARS' } }, tx) + await tx.commit() +} catch (e) { + await tx.rollback() + throw e +} +``` + + + + +```bash +# 1. Begin a transaction +TX_ID=$(curl -s -X POST https://api.rushdb.com/api/v1/tx \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"ttl": 10000}' | jq -r '.data.id') + +# 2. Create records using the transaction header +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "X-Transaction-Id: $TX_ID" \ + -d '{"label": "MOVIE", "data": {"title": "Dune"}}' + +# 3. Commit +curl -X POST https://api.rushdb.com/api/v1/tx/$TX_ID/commit \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +--- + +## TypeScript: Via Model + +```typescript +const MovieModel = new Model('MOVIE', { + title: { type: 'string' }, + rating: { type: 'number' } +}) + +// Create +const movie = await MovieModel.create({ title: 'Inception', rating: 8.8 }) + +// Create many +const movies = await MovieModel.createMany([{ title: 'Dune' }, { title: 'Arrival' }]) + +// Partial update +await MovieModel.update('movie-id-123', { rating: 9.1 }) + +// Full replace +await MovieModel.set('movie-id-123', { title: 'Inception', rating: 9.1, genre: 'sci-fi' }) + +// Delete +await MovieModel.deleteById(['id-1', 'id-2']) +await MovieModel.delete({ where: { genre: 'temp' } }) +``` + +--- + +## See also + +- [Import Data](/build/data/import-data) — import nested JSON or CSV (auto-creates relationships) +- [Find & Query](/build/data/find-and-query) — search and filter records +- [Connect Records](/build/graph/connect-records) — attach and detach relationships +- [Transactions](/build/reliability/transactions) — ACID guarantees for multi-step writes +- [Write Records with Vectors](/build/ai-search/write-with-vectors) — attach embedding vectors diff --git a/docs/docs/learn/records-and-queries/transactions.mdx b/docs/docs/learn/records-and-queries/transactions.mdx new file mode 100644 index 00000000..7c9a4068 --- /dev/null +++ b/docs/docs/learn/records-and-queries/transactions.mdx @@ -0,0 +1,211 @@ +--- +slug: /build/reliability/transactions +sidebar_position: 1 +title: Transactions +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Transactions + +Group multiple write operations into an atomic unit — all succeed together or all roll back together. + +--- + +## Basic Pattern + + + + +The idiomatic approach uses the context manager — auto-commits on success, auto-rolls back on exception: + +```python +# Context manager (recommended) +with db.tx.begin() as tx: + leo = db.records.create(label="ACTOR", data={"name": "Leonardo DiCaprio"}, transaction=tx) + inception = db.records.create(label="MOVIE", data={"title": "Inception"}, transaction=tx) + db.records.attach(source=leo, target=inception, options={"type": "ACTED_IN"}, transaction=tx) +# committed automatically — no explicit commit() needed +``` + +Or manually for more control: + +```python +tx = db.tx.begin() +try: + movie = db.records.create(label="MOVIE", data={"title": "Inception"}, transaction=tx) + actor = db.records.create(label="ACTOR", data={"name": "Leonardo DiCaprio"}, transaction=tx) + db.records.attach(source=movie, target=actor, options={"type": "STARS_IN"}, transaction=tx) + tx.commit() +except Exception: + tx.rollback() + raise +``` + + + + +```typescript +const tx = await db.tx.begin() +// optional: db.tx.begin({ ttl: 10_000 }) + +try { + const movie = await db.records.create({ label: 'MOVIE', data: { title: 'Dune', rating: 8.0 } }, tx) + + const actor = await db.records.create({ label: 'ACTOR', data: { name: 'Timothée Chalamet' } }, tx) + + await db.records.attach({ source: movie, target: actor, options: { type: 'STARS' } }, tx) + + await tx.commit() // or: await db.tx.commit(tx) +} catch (e) { + await tx.rollback() // or: await db.tx.rollback(tx) + throw e +} +``` + +Pass `tx` as the **last argument** to any record or relationship method. + + + + +Use `X-Transaction-Id` header on any write endpoint: + +```bash +# 1. Begin +TX_ID=$(curl -s -X POST https://api.rushdb.com/api/v1/tx \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"ttl": 10000}' | jq -r '.data.id') + +# 2. Create records +MOVIE_ID=$(curl -s -X POST https://api.rushdb.com/api/v1/records \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "X-Transaction-Id: $TX_ID" \ + -d '{"label": "MOVIE", "data": {"title": "Inception"}, "options": {"returnResult": true}}' | jq -r '.data.__id') + +ACTOR_ID=$(curl -s -X POST https://api.rushdb.com/api/v1/records \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "X-Transaction-Id: $TX_ID" \ + -d '{"label": "ACTOR", "data": {"name": "Leonardo DiCaprio"}, "options": {"returnResult": true}}' | jq -r '.data.__id') + +# 3. Link +curl -X POST https://api.rushdb.com/api/v1/records/$MOVIE_ID/relationships \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "X-Transaction-Id: $TX_ID" \ + -d '{"targetIds": "'$ACTOR_ID'", "type": "STARS_IN"}' + +# 4. Commit +curl -X POST https://api.rushdb.com/api/v1/tx/$TX_ID/commit \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +--- + +## API Reference + + + + +| Method | Description | +| ------------------------------ | ------------------------------------------------------- | +| `db.tx.begin(ttl?)` as ctx mgr | Start transaction, auto-commit/rollback on scope exit | +| `db.tx.begin(ttl?)` | Start a new transaction; returns a `Transaction` object | +| `tx.commit()` | Persist all operations | +| `tx.rollback()` | Discard all operations | + + + + +| Method | Description | +| ----------------------- | ------------------------------------------------------------ | +| `db.tx.begin({ ttl? })` | Start a transaction. Returns a `Transaction` with `.id` | +| `db.tx.get(tx)` | Check if a transaction still exists (returns 404 if expired) | +| `db.tx.commit(tx)` | Commit — makes all changes permanent | +| `db.tx.rollback(tx)` | Rollback — discards all changes | +| `tx.commit()` | Shorthand on the transaction object itself | +| `tx.rollback()` | Shorthand on the transaction object itself | + + + + +| Endpoint | Description | +| -------------------------------- | ------------------------------------ | +| `POST /api/v1/tx` | Begin transaction. Returns `{ id }` | +| `GET /api/v1/tx/:txId` | Check if transaction still exists | +| `POST /api/v1/tx/:txId/commit` | Commit — makes all changes permanent | +| `POST /api/v1/tx/:txId/rollback` | Rollback — discards all changes | + +Add `X-Transaction-Id: $TX_ID` to any create, update, delete, or relationship request. + + + + +--- + +## Timeouts + +Uncommitted transactions are automatically rolled back after the TTL expires. + +| Parameter | Value | +| ----------- | ----------- | +| Default TTL | `5 000` ms | +| Maximum TTL | `30 000` ms | + + + + +```python +tx = db.tx.begin(ttl=15000) # 15 s timeout +``` + + + + +```typescript +const tx = await db.tx.begin({ ttl: 15_000 }) // 15 s timeout +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/tx \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"ttl": 15000}' +``` + +| Field | Type | Description | +| ----- | -------- | ---------------------------------------- | +| `ttl` | `number` | TTL in ms. Default: `5000`. Max: `30000` | + + + + +--- + +## Supported Operations + +All record and relationship write operations support transactions: + +| Category | Operations | +| ------------- | ----------------------------------------------------------------------------------------------------- | +| Records | `create` / `create_many` / `createMany` · `update` · `set` · `delete` / `delete_by_id` / `deleteById` | +| Relationships | `attach` / `detach` | +| Reads | `find` (within transaction — reads your uncommitted writes) | + +--- + +## See also + +- [Store Records](/build/data/store-records) — in-transaction create, update, delete examples +- [Connect Records](/build/graph/connect-records) — in-transaction attach, detach examples +- [Find & Query](/build/data/find-and-query) — in-transaction read examples +- [TypeScript Transaction type](/reference/typescript/Transaction) diff --git a/docs/docs/learn/reference/mcp/index.mdx b/docs/docs/learn/reference/mcp/index.mdx new file mode 100644 index 00000000..88a69deb --- /dev/null +++ b/docs/docs/learn/reference/mcp/index.mdx @@ -0,0 +1,46 @@ +--- +slug: /reference/mcp/ +sidebar_position: 0 +title: MCP Server +--- + +# MCP Server Reference + +RushDB exposes a Model Context Protocol server for agents that need to discover, query, mutate, and traverse a project graph. Each tool publishes its input and output JSON schemas to the connected MCP client. + +Use [Connect with MCP](/connect/mcp) for hosted OAuth and local `npx` setup instructions. + +## Endpoint + +Hosted MCP clients connect to: + +```text +https://mcp.rushdb.com/mcp +``` + +Local MCP clients run: + +```bash +npx -y @rushdb/mcp-server +``` + +## Start Here + +The server exposes a discovery-first workflow: + +1. Call `getOntologyMarkdown` once at the start of a session. +2. Call `getSearchQuerySpec` before building queries with dates, metrics, `groupBy`, relationship traversal, or vectors. +3. Use exact label and property names returned by discovery tools. +4. Preview and confirm destructive operations before calling them. + +## Reference + +| Page | Description | +| ------------------------------------------------------------------ | ------------------------------------------------------------- | +| [All MCP Tools](/reference/mcp/tools) | Complete catalog of all 37 tools, grouped by task | +| [MCP Setup](/connect/mcp) | Hosted OAuth, local client configuration, and troubleshooting | +| [Suggested Relationship Patterns](/build/graph/suggested-patterns) | Review and apply inferred relationship structures | + +## Prompt + +Clients with MCP Prompts support can fetch `rushdb.queryBuilder`. Clients without prompt support can call `getQueryBuilderPrompt` to retrieve the same discovery-first instructions. diff --git a/docs/docs/learn/reference/mcp/tools.mdx b/docs/docs/learn/reference/mcp/tools.mdx new file mode 100644 index 00000000..b2817f36 --- /dev/null +++ b/docs/docs/learn/reference/mcp/tools.mdx @@ -0,0 +1,98 @@ +--- +slug: /reference/mcp/tools +sidebar_position: 1 +title: All Tools +--- + +# MCP Tools + +RushDB MCP exposes 37 tools. Names below use the exact lower camel case identifiers published in the MCP manifest. + +The connected MCP client receives the full JSON schema for each tool. This page is the quick reference for choosing the correct tool and understanding its main inputs. + +## Safety Levels + +| Level | Meaning | +| ----------- | ------------------------------------------------------- | +| Read | Retrieves or computes data without changing the project | +| Write | Creates or updates project data | +| Destructive | Deletes or replaces data; preview and confirm first | + +An asterisk marks a required input. + +## Discovery and Properties + +| Tool | Level | Main inputs | Purpose | +| ----------------------- | ----------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| `getOntologyMarkdown` | Read | `labels?`, `force?` | Return the complete graph ontology as compact Markdown. Call once at session start. | +| `getOntology` | Read | `labels?`, `force?` | Return structured ontology JSON, including property IDs and vector indexes. | +| `findLabels` | Read | `where?`, `limit?`, `skip?`, `orderBy?` | List or filter record labels and counts. | +| `findProperties` | Read | `where?`, `limit?`, `skip?`, `orderBy?` | Discover property names, types, IDs, and record counts. | +| `findPropertyById` | Read | `propertyId*` | Fetch metadata for one property. | +| `propertyValues` | Read | `propertyId*`, `query?`, `orderBy?`, `limit?`, `skip?` | Return numeric or date ranges, or distinct string and boolean values. | +| `deleteProperty` | Destructive | `propertyId*` | Permanently remove a property and its values from all records. | +| `getSearchQuerySpec` | Read | none | Return the complete SearchQuery syntax reference. | +| `getQueryBuilderPrompt` | Read | none | Return the built-in discovery-first query builder prompt. | + +## Records + +| Tool | Level | Main inputs | Purpose | +| ------------------- | ----------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `findRecords` | Read | `labels?`, `where?`, `limit?`, `skip?`, `orderBy?`, `select?`, `groupBy?` | Search records and run grouped metrics queries. | +| `findOneRecord` | Read | `labels?`, `where?` | Return the first matching record for entity resolution probes. | +| `findUniqRecord` | Read | `labels?`, `where?` | Return exactly one matching record; fail when zero or multiple records match. | +| `getRecord` | Read | `recordId*` | Fetch one record by ID. | +| `getRecordsByIds` | Read | `recordIds*` | Fetch multiple records by ID. | +| `createRecord` | Write | `label*`, `data*`, `options?`, `transactionId?` | Insert or upsert one record. | +| `updateRecord` | Write | `recordId*`, `label*`, `data*`, `transactionId?` | Partially update one record while preserving unspecified fields. | +| `setRecord` | Destructive | `recordId*`, `label*`, `data*`, `transactionId?` | Replace all fields on one record. | +| `deleteRecord` | Destructive | `recordId*`, `transactionId?` | Delete one record by ID after previewing it with `findRecords`. | +| `deleteRecordById` | Destructive | `recordId*`, `transactionId?` | Delete one record by ID after previewing it with `getRecord`. | +| `bulkCreateRecords` | Write | `label*`, `data*`, `options?`, `transactionId?` | Insert or upsert multiple records with the same label. | +| `bulkDeleteRecords` | Destructive | `where*`, `labels?`, `transactionId?` | Delete all records matching a query. Preview with `findRecords` first. | +| `exportRecords` | Read | `labels?`, `where?`, `limit?`, `orderBy?` | Export matching records as CSV. | + +## Relationships + +| Tool | Level | Main inputs | Purpose | +| ------------------- | ----------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `findRelationships` | Read | `where?`, `limit?`, `skip?`, `orderBy?` | Inspect edges and discover graph paths before traversal queries. | +| `attachRelation` | Write | `sourceId*`, `targetId?`, `targetIds?`, `relationType?`, `direction?`, `transactionId?` | Create relationships between existing records. | +| `detachRelation` | Destructive | `sourceId*`, `targetId?`, `targetIds?`, `relationType?`, `direction?`, `transactionId?` | Remove relationships between records. | + +## Suggested Relationship Patterns + +| Tool | Level | Main inputs | Purpose | +| ----------------------------- | ----------- | ------------------------ | ----------------------------------------------------------------------------- | +| `listRelationshipPatterns` | Read | none | List inferred patterns, ontology relationship summaries, and analysis status. | +| `analyzeRelationshipPatterns` | Write | none | Queue ontology analysis. This may invoke the configured LLM. | +| `approveRelationshipPattern` | Write | `id*` | Approve and apply a suggested pattern. | +| `ignoreRelationshipPattern` | Write | `id*` | Ignore a suggested pattern without applying it. | +| `deleteRelationshipPattern` | Destructive | `id*`, `deleteExisting?` | Delete a stored pattern and optionally its materialized relationships. | + +See [Suggested Relationship Patterns](/build/graph/suggested-patterns) for lifecycle details and REST and SDK examples. + +## Semantic Search + +| Tool | Level | Main inputs | Purpose | +| ------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `findEmbeddingIndexes` | Read | none | List embedding index policies configured for the project. | +| `createEmbeddingIndex` | Write | `label*`, `propertyName*`, `sourceType?`, `similarityFunction?`, `dimensions?` | Create a managed or external embedding index policy. | +| `getEmbeddingIndexStats` | Read | `indexId*` | Check indexing progress for an embedding index. | +| `upsertEmbeddingVectors` | Write | `indexId*`, `items*` | Write pre-computed vectors to an external index. | +| `deleteEmbeddingIndex` | Destructive | `indexId*` | Delete an embedding index and its stored vectors. | +| `semanticSearch` | Read | `propertyName*`, `labels*`, `query?`, `queryVector?`, `sourceType?`, `similarityFunction?`, `where?`, `topK?`, `limit?`, `skip?` | Rank records by vector similarity, optionally after exact filtering. | + +## Utilities + +| Tool | Level | Main inputs | Purpose | +| ----------------- | ----- | ----------- | ------------------------------------------------------ | +| `helpAddToClient` | Read | none | Return instructions for adding RushDB MCP to a client. | + +## Query Rules + +- Never guess labels or property names. Start with `getOntologyMarkdown`. +- Call `getSearchQuerySpec` before complex `findRecords` queries. +- Do not pass `limit` with `select`; it restricts the record scan and produces incorrect metrics. +- For a simple count, read `total` from `findRecords`. +- Preview and explicitly confirm destructive operations. diff --git a/docs/docs/learn/reference/python/RushDB.md b/docs/docs/learn/reference/python/RushDB.md new file mode 100644 index 00000000..bcf1fb1e --- /dev/null +++ b/docs/docs/learn/reference/python/RushDB.md @@ -0,0 +1,193 @@ +--- +slug: /reference/python/RushDB +sidebar_position: 0 +--- + +# RushDB + +The `RushDB` class is the main entry point for interacting with RushDB from Python. It manages authentication, HTTP communication, and exposes all API namespaces. + +## Initialization + +```python +from rushdb import RushDB + +# Connect to RushDB Cloud +db = RushDB("RUSHDB_API_KEY") + +# Connect to a self-hosted instance +db = RushDB( + api_key="RUSHDB_API_KEY", + url="https://your-rushdb-instance.com" +) +``` + +## Constructor + +```python +RushDB(api_key: str, url: Optional[str] = None, base_url: Optional[str] = None) +``` + +| Parameter | Type | Description | +| ---------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `api_key` | `str` | API key for authentication | +| `url` | `str` | Base URL of a self-hosted RushDB instance. When provided without `/api/`, the path `/api/v1` is appended automatically. Defaults to `https://api.rushdb.com/api/v1` | +| `base_url` | `str` | Deprecated alias for `url`. Kept for backwards compatibility | + +## Namespaces + +All database operations are accessed through sub-namespaces on the client instance. + +### `db.records` + +CRUD and bulk operations on records. + +| Method | Description | +| ------------------------------------------------------------------------- | ---------------------------------------------------------- | +| `create(label, data, *, options, vectors, transaction)` | Create a single record | +| `create_many(label, data, *, options, vectors, transaction)` | Create multiple flat records | +| `import_json(data, label, *, options, transaction)` | Import nested/complex JSON payloads | +| `import_csv(label, data, *, options, parse_config, vectors, transaction)` | Import records from CSV text | +| `upsert(data, label, *, options, vectors, transaction)` | Create or update a record | +| `set(target, data, *, label, vectors, transaction)` | Replace all fields of a record | +| `update(target, data, *, transaction)` | Partially update a record | +| `find(search_query, *, record_id, transaction)` | Search records; returns `SearchResult` | +| `find_one(search_query, *, transaction)` | Return the first match or `None` | +| `find_uniq(search_query, *, transaction)` | Return the single match or `None`; raises if more than one | +| `find_by_id(target, *, transaction)` | Fetch record(s) by ID | +| `delete(search_query, *, transaction)` | Delete all records matching a query | +| `delete_by_id(target, *, transaction)` | Delete record(s) by ID | +| `attach(source, target, *, options, transaction)` | Create relationships between records | +| `detach(source, target, *, options, transaction)` | Remove relationships between records | +| `export(search_query, *, transaction)` | Export matching records as CSV text | + +### `db.tx` + +Transaction lifecycle management. + +| Method | Description | +| ----------------------- | ---------------------------------------------- | +| `begin(ttl)` | Start a new transaction; returns `Transaction` | +| `get(transaction)` | Retrieve an existing transaction by ID | +| `commit(transaction)` | Commit a transaction by object or ID | +| `rollback(transaction)` | Roll back a transaction by object or ID | + +See also: [Transaction](/reference/python/transaction) for the `commit()` / `rollback()` instance methods and context manager usage. + +### `db.labels` + +Discover record labels in the database. + +| Method | Description | +| ------------------------------------ | ---------------------------------------------------- | +| `find(search_query, *, transaction)` | Return a dict of `{label: count}` matching the query | + +### `db.properties` + +Inspect property metadata. + +| Method | Description | +| -------------------------------------------------------- | ------------------------------------ | +| `find(search_query, *, transaction)` | List all properties matching a query | +| `find_by_id(property_id, *, transaction)` | Retrieve a property by ID | +| `delete(property_id, *, transaction)` | Delete a property by ID | +| `find_values(property_id, *, search_query, transaction)` | List distinct values for a property | + +### `db.relationships` + +Query and bulk-create relationships. + +| Method | Description | +| ---------------------------------------------------------------------------- | ----------------------------------------------------------- | +| `find(search_query, *, pagination, transaction)` | Search relationships; returns `List[Relationship]` | +| `create_many(*, source, target, type, direction, many_to_many, transaction)` | Bulk-create relationships by key-match or cartesian product | + +### `db.ai` + +Semantic search and ontology exploration. Embedding index management is available under `db.ai.indexes`. + +| Method | Description | +| ----------------------------------------------- | ----------------------------------------------------------------------- | +| `search(params)` | Semantic (vector) search over indexed properties; returns `ApiResponse` | +| `get_ontology(params, *, transaction)` | Return the full graph ontology as structured JSON | +| `get_ontology_markdown(params, *, transaction)` | Return the ontology as compact Markdown (token-efficient, for LLMs) | + +#### `db.ai.indexes` + +| Method | Description | +| ---------------------------------- | ----------------------------------------------------- | +| `find()` | List all embedding index policies | +| `create(params)` | Create a new embedding index for a property | +| `delete(index_id)` | Delete an embedding index by ID | +| `stats(index_id)` | Get Neo4j-level statistics for an index | +| `upsert_vectors(index_id, params)` | Bulk-seed an external index with pre-computed vectors | + +### `db.query` + +Raw Cypher query execution. **Cloud-only** — not available on self-hosted instances without a managed database. + +| Method | Description | +| --------------------------- | ---------------------------------------------------------- | +| `raw(body, *, transaction)` | Execute a raw Cypher query string with optional parameters | + +### `db.settings` + +Project configuration. + +| Method | Description | +| ------- | ------------------------------------- | +| `get()` | Retrieve the current project settings | + +## Instance Methods + +### `ping()` + +```python +def ping() -> bool +``` + +Tests connectivity to the RushDB server. Returns `True` if the server is reachable, `False` otherwise. Safe to use in conditional checks — never raises. + +```python +if db.ping(): + print("Connected") +else: + raise RuntimeError("Cannot reach RushDB server") +``` + +## Usage Example + +```python +from rushdb import RushDB + +db = RushDB("RUSHDB_API_KEY") + +# Create records +user = db.records.create("User", {"name": "Alice", "email": "alice@example.com"}) +post = db.records.create("Post", {"title": "Hello World", "published": True}) + +# Connect them +db.records.attach(user, post, options={"type": "AUTHORED"}) + +# Query +results = db.records.find({ + "labels": ["User"], + "where": {"email": {"$contains": "@example.com"}}, + "limit": 10 +}) + +for record in results: + print(record["name"]) + +# Transaction +tx = db.tx.begin(ttl=10000) +try: + db.records.create("Log", {"event": "login"}, transaction=tx) + tx.commit() +except Exception: + tx.rollback() + +# Or use the context manager — auto-commits on success, auto-rolls back on error +with db.tx.begin() as tx: + db.records.create("Log", {"event": "login"}, transaction=tx) +``` diff --git a/docs/docs/python-sdk/python-reference/SearchQuery.md b/docs/docs/learn/reference/python/SearchQuery.md similarity index 79% rename from docs/docs/python-sdk/python-reference/SearchQuery.md rename to docs/docs/learn/reference/python/SearchQuery.md index 794a21d0..b2fc9e0b 100644 --- a/docs/docs/python-sdk/python-reference/SearchQuery.md +++ b/docs/docs/learn/reference/python/SearchQuery.md @@ -1,10 +1,11 @@ --- +slug: /reference/python/SearchQuery sidebar_position: 4 --- # SearchQuery -`SearchQuery` is a dictionary type that defines the structure for querying [records](../../concepts/records) in RushDB. It provides a flexible way to filter, sort, paginate, and shape results. For more information on search concepts, see the [search documentation](../../concepts/search/introduction.md). +`SearchQuery` is a dictionary type that defines the structure for querying [records](/build/data/store-records) in RushDB. It provides a flexible way to filter, sort, paginate, and shape results. For more information on search concepts, see the [search documentation](/reference/search-query). ## Query Shape @@ -32,10 +33,10 @@ Specifies which record types to search. Multiple labels are combined with OR. If ### Pagination -| Field | Type | Description | -|---------|-------|------------------------------------------| -| `limit` | `int` | Maximum number of records to return | -| `skip` | `int` | Number of records to skip (for paging) | +| Field | Type | Description | +| ------- | ----- | -------------------------------------- | +| `limit` | `int` | Maximum number of records to return | +| `skip` | `int` | Number of records to skip (for paging) | ### Order @@ -50,16 +51,16 @@ The `where` dictionary filters records based on property values and relationship #### Number Operators -| Operator | Meaning | -|----------|-----------------------| -| `$gt` | Greater than | -| `$gte` | Greater than or equal | -| `$lt` | Less than | -| `$lte` | Less than or equal | -| `$ne` | Not equal | -| `$in` | Matches any in list | -| `$nin` | Matches none in list | -| `$exists`| Field exists / absent | +| Operator | Meaning | +| --------- | --------------------- | +| `$gt` | Greater than | +| `$gte` | Greater than or equal | +| `$lt` | Less than | +| `$lte` | Less than or equal | +| `$ne` | Not equal | +| `$in` | Matches any in list | +| `$nin` | Matches none in list | +| `$exists` | Field exists / absent | ```python { "where": { "age": { "$gte": 21, "$lt": 65 } } } @@ -68,15 +69,15 @@ The `where` dictionary filters records based on property values and relationship #### String Operators -| Operator | Meaning | -|---------------|--------------------------------------| -| `$contains` | Substring match (case-insensitive) | -| `$startsWith` | Prefix match (case-insensitive) | -| `$endsWith` | Suffix match (case-insensitive) | -| `$ne` | Not equal | -| `$in` | Matches any value in list | -| `$nin` | Matches none of the values | -| `$exists` | Field exists / absent | +| Operator | Meaning | +| ------------- | ---------------------------------- | +| `$contains` | Substring match (case-insensitive) | +| `$startsWith` | Prefix match (case-insensitive) | +| `$endsWith` | Suffix match (case-insensitive) | +| `$ne` | Not equal | +| `$in` | Matches any value in list | +| `$nin` | Matches none of the values | +| `$exists` | Field exists / absent | ```python { "where": { "name": { "$contains": "John" } } } @@ -122,6 +123,7 @@ Never use plain ISO strings with `$gt` / `$lt`: # Records from the 1990s { "where": { "publishedAt": { "$gte": { "$year": 1990 }, "$lt": { "$year": 2000 } } } } ``` + ::: #### Type Expression @@ -154,13 +156,13 @@ result = db.records.find({ #### Logical Operators -| Operator | Meaning | -|----------|--------------------------------| -| `$and` | All conditions must match | -| `$or` | At least one must match | -| `$not` | Condition must NOT match | -| `$nor` | None of the conditions match | -| `$xor` | Exactly one condition matches | +| Operator | Meaning | +| -------- | ----------------------------- | +| `$and` | All conditions must match | +| `$or` | At least one must match | +| `$not` | Condition must NOT match | +| `$nor` | None of the conditions match | +| `$xor` | Exactly one condition matches | ```python # Implicit AND (multiple keys at same level) @@ -252,20 +254,20 @@ Copy a field value into the output row: ### Expressions -| Expression | Description | -|---|---| -| `{ "$sum": expr }` | Sum of a numeric expression | -| `{ "$avg": expr, "$precision": n }` | Average with optional decimal precision | -| `{ "$count": "*" \| expr }` | Count: `"*"` = root records; expr = distinct values | -| `{ "$min": expr }` | Minimum value | -| `{ "$max": expr }` | Maximum value | -| `{ "$divide": [expr, expr] }` | Division | -| `{ "$multiply": [expr, expr] }` | Multiplication | -| `{ "$add": [expr, expr] }` | Addition | -| `{ "$subtract": [expr, expr] }` | Subtraction | -| `{ "$ref": "key" }` | Reference another output key in the same `select` map | -| `{ "$collect": CollectExpr }` | Collect related records into an array | -| `{ "$timeBucket": TimeBucketExpr }` | Bucket a datetime field into calendar intervals | +| Expression | Description | +| ----------------------------------- | ----------------------------------------------------- | +| `{ "$sum": expr }` | Sum of a numeric expression | +| `{ "$avg": expr, "$precision": n }` | Average with optional decimal precision | +| `{ "$count": "*" \| expr }` | Count: `"*"` = root records; expr = distinct values | +| `{ "$min": expr }` | Minimum value | +| `{ "$max": expr }` | Maximum value | +| `{ "$divide": [expr, expr] }` | Division | +| `{ "$multiply": [expr, expr] }` | Multiplication | +| `{ "$add": [expr, expr] }` | Addition | +| `{ "$subtract": [expr, expr] }` | Subtraction | +| `{ "$ref": "key" }` | Reference another output key in the same `select` map | +| `{ "$collect": CollectExpr }` | Collect related records into an array | +| `{ "$timeBucket": TimeBucketExpr }` | Bucket a datetime field into calendar intervals | ```python # Per-company employee statistics @@ -287,16 +289,16 @@ result = db.records.find({ Two forms — `from` (alias-based) and `label` (inline traversal, preferred for nesting): -| Option | Type | Description | -|-----------|-----------|----------------------------------------------------------------------| -| `from` | `str` | `"$alias"` — alias declared in `where` (alias-based form) | -| `label` | `str` | Related record label to traverse to (label-based form; no alias needed) | -| `where` | `dict` | Flat property filter on this traversal level (label-based only) | -| `select` | `dict` | Field projection; nested `$collect` allowed (label-based) | -| `unique` | `bool` | Deduplicate (default `True`) | -| `limit` | `int` | Max items in the collected list | -| `skip` | `int` | Skip N items in the collected list | -| `orderBy` | `dict` | Sort collected items | +| Option | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------- | +| `from` | `str` | `"$alias"` — alias declared in `where` (alias-based form) | +| `label` | `str` | Related record label to traverse to (label-based form; no alias needed) | +| `where` | `dict` | Flat property filter on this traversal level (label-based only) | +| `select` | `dict` | Field projection; nested `$collect` allowed (label-based) | +| `unique` | `bool` | Deduplicate (default `True`) | +| `limit` | `int` | Max items in the collected list | +| `skip` | `int` | Skip N items in the collected list | +| `orderBy` | `dict` | Sort collected items | > Use `"$self"` in `select` to reference the current traversal level when using `label`. > `from` and `label` are mutually exclusive. diff --git a/docs/docs/learn/reference/python/index.md b/docs/docs/learn/reference/python/index.md new file mode 100644 index 00000000..921abacc --- /dev/null +++ b/docs/docs/learn/reference/python/index.md @@ -0,0 +1,55 @@ +--- +slug: /reference/python/ +sidebar_position: 0 +title: Python SDK +--- + +# Python SDK + +The official Python SDK for RushDB. Compatible with Python 3.8+. + +## Installation + +```bash +pip install rushdb +``` + +## Quick Start + +```python +from rushdb import RushDB + +db = RushDB("RUSHDB_API_KEY") + +# Store a record +db.records.create( + label="User", + data={"name": "Alice", "role": "engineer"}, +) + +# Query records +result = db.records.find({ + "labels": ["User"], + "where": {"role": "engineer"}, + "limit": 10, +}) +``` + +To connect to a self-hosted instance pass the `url` parameter: + +```python +db = RushDB( + api_key="RUSHDB_API_KEY", + url="https://your-rushdb-instance.com", +) +``` + +## API Reference + +| Class | Description | +| ----------------------------------------------- | --------------------------------------------------------------------------------- | +| [RushDB](/reference/python/RushDB) | Main client — entry point for all operations | +| [Record](/reference/python/record) | Typed record class with instance methods (`attach`, `detach`, `update`, `delete`) | +| [SearchQuery](/reference/python/SearchQuery) | Query builder types and interfaces | +| [SearchResult](/reference/python/search-result) | Paginated result wrapper returned by `find` and `ai.search` | +| [Transaction](/reference/python/transaction) | ACID transaction handle | diff --git a/docs/docs/python-sdk/python-reference/record.md b/docs/docs/learn/reference/python/record.md similarity index 99% rename from docs/docs/python-sdk/python-reference/record.md rename to docs/docs/learn/reference/python/record.md index 5bf9b5e6..c7a2c434 100644 --- a/docs/docs/python-sdk/python-reference/record.md +++ b/docs/docs/learn/reference/python/record.md @@ -1,4 +1,5 @@ --- +slug: /reference/python/record sidebar_position: 2 --- @@ -23,6 +24,7 @@ Gets the record's unique identifier. **Type:** `str` **Example:** + ```python record = client.records.create("USER", {"name": "John"}) print(record.id) # e.g., "1234abcd-5678-..." @@ -35,6 +37,7 @@ Gets the record's property types. **Type:** `str` **Example:** + ```python record = client.records.create("USER", {"name": "John", "age": 25}) print(record.proptypes) # Returns property type definitions @@ -47,6 +50,7 @@ Gets the record's label. **Type:** `str` **Example:** + ```python record = client.records.create("USER", {"name": "John"}) print(record.label) # "USER" @@ -59,6 +63,7 @@ Gets the record's creation timestamp from its ID. **Type:** `int` **Example:** + ```python record = client.records.create("USER", {"name": "John"}) print(record.timestamp) # Unix timestamp in milliseconds @@ -71,6 +76,7 @@ Gets the record's creation date. **Type:** `datetime` **Example:** + ```python record = client.records.create("USER", {"name": "John"}) print(record.date) # datetime object @@ -83,6 +89,7 @@ Relevance score from a vector/semantic search result. Returns `None` for records **Type:** `Optional[float]` **Example:** + ```python results = db.ai.search({"query": "machine learning", "propertyName": "content", "labels": ["DOC"]}) for doc in results.data: @@ -100,6 +107,7 @@ Returns `True` if the record has a valid `__id`, `False` otherwise. **Type:** `bool` (property — no parentheses) **Example:** + ```python record = db.records.create("USER", {"name": "John"}) if record.exists: @@ -116,6 +124,7 @@ Returns user-defined fields only, excluding all internal `__*` fields. **Type:** `Dict[str, Any]` **Example:** + ```python record = db.records.create("USER", {"name": "John", "age": 30}) print(record.fields) # {"name": "John", "age": 30} @@ -126,14 +135,17 @@ print(record.fields) # {"name": "John", "age": 30} Converts this record to a `pandas.Series`. Requires `pandas` to be installed. **Signature:** + ```python def to_series(self, exclude_internal: bool = True) -> pandas.Series ``` **Arguments:** + - `exclude_internal` (bool): If `True` (default), fields starting with `__` are excluded. **Example:** + ```python import pandas as pd @@ -161,6 +173,7 @@ df = results.to_dataframe(exclude_internal=False) # includes __id, __label, etc Updates all data for the record. **Signature:** + ```python def set( self, @@ -170,13 +183,16 @@ def set( ``` **Arguments:** + - `data` (Dict[str, Any]): New record data - `transaction` (Optional[Transaction]): Optional transaction object **Returns:** + - `Dict[str, str]`: Response data **Example:** + ```python record = client.records.create("USER", {"name": "John"}) response = record.set({ @@ -191,6 +207,7 @@ response = record.set({ Updates specific fields of the record. **Signature:** + ```python def update( self, @@ -200,13 +217,16 @@ def update( ``` **Arguments:** + - `data` (Dict[str, Any]): Partial record data to update - `transaction` (Optional[Transaction]): Optional transaction object **Returns:** + - `Dict[str, str]`: Response data **Example:** + ```python record = client.records.create("USER", { "name": "John", @@ -222,6 +242,7 @@ response = record.update({ Creates relationships with other records. **Signature:** + ```python def attach( self, @@ -232,6 +253,7 @@ def attach( ``` **Arguments:** + - `target` (Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], Record, List[Record]]): Target record(s) - `options` (Optional[RelationshipOptions]): Relationship options - `direction` (Optional[Literal["in", "out"]]): Relationship direction @@ -239,9 +261,11 @@ def attach( - `transaction` (Optional[Transaction]): Optional transaction object **Returns:** + - `Dict[str, str]`: Response data **Example:** + ```python # Create two records user = client.records.create("USER", {"name": "John"}) @@ -262,6 +286,7 @@ response = user.attach( Removes relationships with other records. **Signature:** + ```python def detach( self, @@ -272,6 +297,7 @@ def detach( ``` **Arguments:** + - `target` (Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], Record, List[Record]]): Target record(s) - `options` (Optional[RelationshipDetachOptions]): Detach options - `direction` (Optional[Literal["in", "out"]]): Relationship direction @@ -279,9 +305,11 @@ def detach( - `transaction` (Optional[Transaction]): Optional transaction object **Returns:** + - `Dict[str, str]`: Response data **Example:** + ```python # Detach user from group response = user.detach( @@ -298,6 +326,7 @@ response = user.detach( Deletes the record. **Signature:** + ```python def delete( self, @@ -306,12 +335,15 @@ def delete( ``` **Arguments:** + - `transaction` (Optional[Transaction]): Optional transaction object **Returns:** + - `Dict[str, str]`: Response data **Example:** + ```python user = client.records.create("USER", {"name": "John"}) response = user.delete() diff --git a/docs/docs/python-sdk/python-reference/search-result.md b/docs/docs/learn/reference/python/search-result.md similarity index 99% rename from docs/docs/python-sdk/python-reference/search-result.md rename to docs/docs/learn/reference/python/search-result.md index c5723c53..783f205c 100644 --- a/docs/docs/python-sdk/python-reference/search-result.md +++ b/docs/docs/learn/reference/python/search-result.md @@ -1,4 +1,5 @@ --- +slug: /reference/python/search-result sidebar_position: 3 --- @@ -544,6 +545,7 @@ print(f"Loaded {len(result)} out of {result.total} total users") ``` 5. **Use Skip and Limit for Pagination**: Leverage the `skip` and `limit` properties for implementing pagination: + ```python # Get next page next_result = client.records.find({ @@ -554,6 +556,7 @@ print(f"Loaded {len(result)} out of {result.total} total users") ``` Or use the built-in `next()` method: + ```python page2 = result.next() ``` diff --git a/docs/docs/learn/reference/python/transaction.md b/docs/docs/learn/reference/python/transaction.md new file mode 100644 index 00000000..e18df593 --- /dev/null +++ b/docs/docs/learn/reference/python/transaction.md @@ -0,0 +1,130 @@ +--- +slug: /reference/python/transaction +sidebar_position: 1 +--- + +# Transaction + +A `Transaction` groups multiple database operations so they succeed or fail together. Obtain one via `db.tx.begin()`, then pass it to any write method. Call `commit()` to make changes permanent or `rollback()` to discard them. + +## Class Definition + +```python +class Transaction: + def __init__(self, client: "RushDB", transaction_id: str) +``` + +## Properties + +### id + +```python +id: str +``` + +The server-assigned transaction identifier. Passed automatically via `X-Transaction-Id` header when you supply the `Transaction` object to any API method. + +## Instance Methods + +### `commit()` + +```python +def commit() -> None +``` + +Makes all operations performed within this transaction permanent. Raises `RushDBError` if the transaction has already been committed or rolled back. + +### `rollback()` + +```python +def rollback() -> None +``` + +Discards all operations performed within this transaction. Raises `RushDBError` if the transaction has already been committed or rolled back. + +## Context Manager + +`Transaction` implements `__enter__` / `__exit__`, so you can use it with `with`: + +```python +with db.tx.begin() as tx: + db.records.create("User", {"name": "Alice"}, transaction=tx) + db.records.create("User", {"name": "Bob"}, transaction=tx) +# auto-commits on success; auto-rolls back if an exception is raised +``` + +## `db.tx` — TransactionsAPI + +Transactions are created and managed through the `db.tx` namespace. + +### `begin()` + +```python +def begin(ttl: Optional[int] = None) -> Transaction +``` + +Starts a new transaction. Returns a `Transaction` object. + +| Parameter | Type | Description | +| --------- | ----- | ------------------------------------------------------------------------------------------ | +| `ttl` | `int` | Time-to-live in milliseconds before the transaction auto-expires. Defaults to `5000` (5 s) | + +```python +tx = db.tx.begin(ttl=30000) # 30-second window +``` + +### `get()` + +```python +def get(transaction: Union[str, Transaction]) -> Transaction +``` + +Retrieves an existing transaction by its ID or `Transaction` object. Useful when the original object is unavailable (e.g. recovered from storage or passed across a process boundary). + +### `commit()` + +```python +def commit(transaction: Union[str, Transaction]) -> None +``` + +Commits a transaction identified by a `Transaction` object or a raw ID string. Equivalent to calling `tx.commit()` on the object itself. + +### `rollback()` + +```python +def rollback(transaction: Union[str, Transaction]) -> None +``` + +Rolls back a transaction identified by a `Transaction` object or a raw ID string. + +## Usage Examples + +### Manual commit / rollback + +```python +tx = db.tx.begin(ttl=10000) +try: + db.records.create("User", {"name": "Alice"}, transaction=tx) + db.records.create("User", {"name": "Bob"}, transaction=tx) + tx.commit() +except Exception: + tx.rollback() + raise +``` + +### Context manager (recommended) + +```python +with db.tx.begin() as tx: + user = db.records.create("User", {"name": "Alice"}, transaction=tx) + post = db.records.create("Post", {"title": "Hello"}, transaction=tx) + db.records.attach(user, post, options={"type": "AUTHORED"}, transaction=tx) +``` + +### Recover a transaction by ID + +```python +tx_id = "some-saved-id" +tx = db.tx.get(tx_id) +tx.rollback() +``` diff --git a/docs/docs/rest-api/ai/advanced-indexing.md b/docs/docs/learn/reference/rest-api/ai-and-vectors/advanced-indexing.md similarity index 84% rename from docs/docs/rest-api/ai/advanced-indexing.md rename to docs/docs/learn/reference/rest-api/ai-and-vectors/advanced-indexing.md index c5dcc20c..f310d20f 100644 --- a/docs/docs/rest-api/ai/advanced-indexing.md +++ b/docs/docs/learn/reference/rest-api/ai-and-vectors/advanced-indexing.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/ai/advanced-indexing sidebar_position: 2 title: Advanced Indexing — BYOV --- @@ -52,13 +53,13 @@ An external index starts with status `awaiting_vectors` and transitions to `read ### External vs managed comparison -| | Managed | External | -|---|---|---| -| `sourceType` | `"managed"` | `"external"` | -| Initial status | `"pending"` | `"awaiting_vectors"` | -| Who computes embeddings | RushDB server (configured model) | Your application | -| `dimensions` required | No (uses server default) | **Yes** | -| Backfill for existing records | Automatic | Manual via `upsertVectors` or inline writes | +| | Managed | External | +| ----------------------------- | -------------------------------- | ------------------------------------------- | +| `sourceType` | `"managed"` | `"external"` | +| Initial status | `"pending"` | `"awaiting_vectors"` | +| Who computes embeddings | RushDB server (configured model) | Your application | +| `dimensions` required | No (uses server default) | **Yes** | +| Backfill for existing records | Automatic | Manual via `upsertVectors` or inline writes | --- @@ -70,8 +71,8 @@ The bulk upload API — ideal for seeding an index from a dataset or syncing aft ### Request Body -| Field | Type | Required | Description | -|---------|-------|----------|---------------------------------------------------------| +| Field | Type | Required | Description | +| ------- | ----- | -------- | ------------------------------------------------------------- | | `items` | array | **yes** | Array of `{ "recordId": string, "vector": number[] }` objects | ### Example Request @@ -94,7 +95,7 @@ The request is **idempotent** — calling it again with the same `recordId` repl ## Writing vectors at record creation time -Instead of a two-step create → upsertVectors flow, you can write vectors inline using the `vectors` field on any write endpoint. See [Write Records with Vectors](./write-with-vectors.md) for the full reference. +Instead of a two-step create → upsertVectors flow, you can write vectors inline using the `vectors` field on any write endpoint. See [Write Records with Vectors](/rest-api/ai/write-with-vectors) for the full reference. ```bash # One step: create record AND write its vector @@ -135,12 +136,12 @@ curl -X POST https://api.rushdb.com/api/v1/records \ Two index policies are considered **identical** (and a second create returns `409 Conflict`) when all five fields match: | Field | Effect on uniqueness | -|----------------------|----------------------| -| `label` | ✅ | -| `propertyName` | ✅ | -| `sourceType` | ✅ | -| `similarityFunction` | ✅ | -| `dimensions` | ✅ | +| -------------------- | -------------------- | +| `label` | ✅ | +| `propertyName` | ✅ | +| `sourceType` | ✅ | +| `similarityFunction` | ✅ | +| `dimensions` | ✅ | Changing any one field produces a distinct index and both are allowed to coexist. diff --git a/docs/docs/rest-api/ai/indexing.md b/docs/docs/learn/reference/rest-api/ai-and-vectors/indexing.md similarity index 87% rename from docs/docs/rest-api/ai/indexing.md rename to docs/docs/learn/reference/rest-api/ai-and-vectors/indexing.md index c7f00dfb..158ba8f0 100644 --- a/docs/docs/rest-api/ai/indexing.md +++ b/docs/docs/learn/reference/rest-api/ai-and-vectors/indexing.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/ai/indexing sidebar_position: 1 title: Embedding Indexes --- @@ -72,13 +73,13 @@ Creates a new managed embedding index policy scoped to a label. The property mus ### Request Body -| Field | Type | Required | Description | -|----------------------|--------|----------|-----------------------------------------------------------------------------------------------------------------------| -| `label` | string | **yes** | Label to scope this index to (e.g. `"Article"`, `"Product"`) | -| `propertyName` | string | **yes** | Name of the property to embed (e.g. `"description"`) | -| `sourceType` | string | no | `"managed"` (default) or `"external"`. See [Advanced Indexing](./advanced-indexing.md). | -| `similarityFunction` | string | no | `"cosine"` (default) or `"euclidean"` | -| `dimensions` | number | no | Vector dimensionality. Defaults to server `RUSHDB_EMBEDDING_DIMENSIONS`. **Required** for external indexes. | +| Field | Type | Required | Description | +| -------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------- | +| `label` | string | **yes** | Label to scope this index to (e.g. `"Article"`, `"Product"`) | +| `propertyName` | string | **yes** | Name of the property to embed (e.g. `"description"`) | +| `sourceType` | string | no | `"managed"` (default) or `"external"`. See [Advanced Indexing](/rest-api/ai/advanced-indexing). | +| `similarityFunction` | string | no | `"cosine"` (default) or `"euclidean"` | +| `dimensions` | number | no | Vector dimensionality. Defaults to server `RUSHDB_EMBEDDING_DIMENSIONS`. **Required** for external indexes. | > **Model config is server-side.** The embedding model is set via `RUSHDB_EMBEDDING_MODEL` / `RUSHDB_EMBEDDING_DIMENSIONS` env vars. @@ -124,21 +125,21 @@ curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ ### Index lifecycle -| Status | Description | -|--------------------|-----------------------------------------------------------------| -| `pending` | Policy created, waiting for backfill scheduler | -| `indexing` | Backfill in progress | -| `awaiting_vectors` | External index — waiting for client to push vectors | -| `ready` | All existing records have vectors; search is available | -| `error` | Backfill failed; check server logs for the cause | +| Status | Description | +| ------------------ | ------------------------------------------------------ | +| `pending` | Policy created, waiting for backfill scheduler | +| `indexing` | Backfill in progress | +| `awaiting_vectors` | External index — waiting for client to push vectors | +| `ready` | All existing records have vectors; search is available | +| `error` | Backfill failed; check server logs for the cause | ### Error cases -| Status | When | -|--------|-------------------------------------------------------------------------------------------------| -| `404` | The property does not exist in the project graph | -| `422` | The property exists but is not `string` type | -| `422` | Embedding model is not configured on the server | +| Status | When | +| ------ | ---------------------------------------------------------------------------------------------------------- | +| `404` | The property does not exist in the project graph | +| `422` | The property exists but is not `string` type | +| `422` | Embedding model is not configured on the server | | `409` | An index for this `(label, propertyName, sourceType, similarityFunction, dimensions)` tuple already exists | --- @@ -211,10 +212,10 @@ async function waitForIndexReady(apiKey, indexId, timeoutMs = 90_000) { headers: { Authorization: `Bearer ${apiKey}` } }) const { data: indexes } = await res.json() - const idx = indexes.find(i => i.id === indexId) + const idx = indexes.find((i) => i.id === indexId) if (idx?.status === 'ready') return if (idx?.status === 'error') throw new Error('Index entered error state') - await new Promise(r => setTimeout(r, 3_000)) + await new Promise((r) => setTimeout(r, 3_000)) } throw new Error('Index did not become ready in time') } @@ -240,7 +241,7 @@ curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ -d '{"label": "Product", "propertyName": "description", "similarityFunction": "euclidean", "dimensions": 768}' ``` -When searching or writing vectors against a property with multiple indexes, specify `similarityFunction` to disambiguate. See [Advanced Indexing](./advanced-indexing.md#disambiguation) for details. +When searching or writing vectors against a property with multiple indexes, specify `similarityFunction` to disambiguate. See [Advanced Indexing](/rest-api/ai/advanced-indexing#disambiguation) for details. --- diff --git a/docs/docs/rest-api/ai/overview.md b/docs/docs/learn/reference/rest-api/ai-and-vectors/overview.md similarity index 64% rename from docs/docs/rest-api/ai/overview.md rename to docs/docs/learn/reference/rest-api/ai-and-vectors/overview.md index 548e9ecc..017c23a8 100644 --- a/docs/docs/rest-api/ai/overview.md +++ b/docs/docs/learn/reference/rest-api/ai-and-vectors/overview.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/ai/overview sidebar_position: 0 title: Overview --- @@ -9,11 +10,11 @@ RushDB is a **self-aware memory layer for agents, humans, and apps**. It continu The AI API covers three capabilities: -| Capability | Description | -|---|---| -| **Graph Ontology** | Self-describing schema discovery: label names, field types, value ranges, and the relationship map — always up to date | -| **Embedding Indexes** | Per-label vector policies that turn string properties into long-term semantic memory | -| **Semantic Search** | Cosine/euclidean similarity retrieval over indexed properties, for agents and apps alike | +| Capability | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **Graph Ontology** | Self-describing schema discovery: label names, field types, value ranges, and the relationship map — always up to date | +| **Embedding Indexes** | Per-label vector policies that turn string properties into long-term semantic memory | +| **Semantic Search** | Cosine/euclidean similarity retrieval over indexed properties, for agents and apps alike | --- @@ -56,14 +57,14 @@ The AI API covers three capabilities: ## Quick links -| Topic | Description | -|---|---| -| [Ontology](#graph-ontology) | Schema discovery with `POST /api/v1/ai/ontology/md` and `POST /api/v1/ai/ontology` | -| [Indexing](./indexing.md) | Create and manage managed embedding indexes | -| [Advanced Indexing — BYOV](./advanced-indexing.md) | Bring Your Own Vectors: external indexes, inline writes | -| [Semantic Search](./search.md) | Query by meaning with `POST /api/v1/ai/search` | -| [Writing with Vectors](./write-with-vectors.md) | Attach vectors at create / upsert / importJson time | -| [Agent Skills](#agent-skills) | Installable skills that teach any compatible agent to use RushDB | +| Topic | Description | +| ---------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| [Ontology](#graph-ontology) | Schema discovery with `POST /api/v1/ai/ontology/md` and `POST /api/v1/ai/ontology` | +| [Indexing](/rest-api/ai/indexing) | Create and manage managed embedding indexes | +| [Advanced Indexing — BYOV](/rest-api/ai/advanced-indexing) | Bring Your Own Vectors: external indexes, inline writes | +| [Semantic Search](/rest-api/ai/search) | Query by meaning with `POST /api/v1/ai/search` | +| [Writing with Vectors](/rest-api/ai/write-with-vectors) | Attach vectors at create / upsert / importJson time | +| [Agent Skills](#agent-skills) | Installable skills that teach any compatible agent to use RushDB | --- @@ -79,10 +80,10 @@ Returns the full schema as compact Markdown — the **recommended format for LLM #### Request Body -| Field | Type | Required | Description | -|----------|------------------|----------|-----------------------------------------------------------------------------| +| Field | Type | Required | Description | +| -------- | ---------------- | -------- | ---------------------------------------------------------------------------- | | `labels` | array of strings | no | Restrict output to specific labels. Omit (or pass `[]`) for the full schema. | -| `force` | boolean | no | Bypass the 1-hour cache and force a full recalculation. | +| `force` | boolean | no | Bypass the 1-hour cache and force a full recalculation. | #### Example Request @@ -148,9 +149,9 @@ Returns the same ontology as a structured JSON array. Each element describes one #### Request Body -| Field | Type | Required | Description | -|----------|------------------|----------|--------------------------------------------------------| -| `labels` | array of strings | no | Restrict to specific labels. Omit for the full schema. | +| Field | Type | Required | Description | +| -------- | ---------------- | -------- | ------------------------------------------------------- | +| `labels` | array of strings | no | Restrict to specific labels. Omit for the full schema. | | `force` | boolean | no | Bypass the 1-hour cache and force a full recalculation. | #### Response Schema @@ -162,9 +163,12 @@ Returns the same ontology as a structured JSON array. Each element describes one "count": 1840, "properties": [ { "id": "prop_abc123", "name": "status", "type": "string", "values": ["pending", "paid", "shipped"] }, - { "id": "prop_def456", "name": "total", "type": "number", "min": 4.99, "max": 2499.00 }, + { "id": "prop_def456", "name": "total", "type": "number", "min": 4.99, "max": 2499.0 }, { - "id": "prop_xyz789", "name": "name", "type": "string", "values": ["Widget A", "Widget B"], + "id": "prop_xyz789", + "name": "name", + "type": "string", + "values": ["Widget A", "Widget B"], "vectorIndexes": [ { "id": "idx_001", @@ -178,8 +182,8 @@ Returns the same ontology as a structured JSON array. Each element describes one } ], "relationships": [ - { "label": "User", "type": "PLACED_BY", "direction": "out" }, - { "label": "Product", "type": "CONTAINS", "direction": "out" } + { "label": "User", "type": "PLACED_BY", "direction": "out" }, + { "label": "Product", "type": "CONTAINS", "direction": "out" } ] } ] @@ -211,15 +215,16 @@ Call `POST /api/v1/ai/ontology/md` first in every AI session. Without it, models npx skills add rush-db/rushdb --path packages/skills ``` -| Skill | What it teaches | -|---|---| -| `rushdb-query-builder` | Discovery-first workflow, SearchQuery syntax, aggregation, relationship traversal, and semantic search | -| `rushdb-agent-memory` | Using RushDB as persistent structured memory — store, link, and semantically recall sessions, decisions, and entities | -| `rushdb-data-modeling` | LMPG model design, label/property naming conventions, nested JSON import, and schema evolution | -| `rushdb-faceted-search` | Build faceted filter UIs — discover properties and types, enumerate distinct values, map to widgets, assemble a live `where` clause | +| Skill | What it teaches | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `rushdb-query-builder` | Discovery-first workflow, SearchQuery syntax, aggregation, relationship traversal, and semantic search | +| `rushdb-agent-memory` | Using RushDB as persistent structured memory — store, link, and semantically recall sessions, decisions, and entities | +| `rushdb-data-modeling` | LMPG model design, label/property naming conventions, nested JSON import, and schema evolution | +| `rushdb-faceted-search` | Build faceted filter UIs — discover properties and types, enumerate distinct values, map to widgets, assemble a live `where` clause | +| `rushdb-domain-template` | Design a tailored schema for any domain through guided conversation — interview, entity/relationship mapping, bootstrap payload, and 10 built-in domain templates | Each skill bundles a `SKILL.md` with concise instructions and optional reference files (like the full SearchQuery spec) that the agent loads on demand. :::note MCP server vs. Agent Skills -The [MCP server](/mcp-server/introduction) gives agents direct tool access to RushDB at runtime. Agent Skills teach agents *how* to use those tools correctly — they complement each other. +The [MCP server](/connect/mcp) gives agents direct tool access to RushDB at runtime. Agent Skills teach agents _how_ to use those tools correctly — they complement each other. ::: diff --git a/docs/docs/rest-api/ai/search.md b/docs/docs/learn/reference/rest-api/ai-and-vectors/search.md similarity index 76% rename from docs/docs/rest-api/ai/search.md rename to docs/docs/learn/reference/rest-api/ai-and-vectors/search.md index 7b4f822f..62ff4b8c 100644 --- a/docs/docs/rest-api/ai/search.md +++ b/docs/docs/learn/reference/rest-api/ai-and-vectors/search.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/ai/search sidebar_position: 3 title: Semantic Search --- @@ -15,17 +16,17 @@ RushDB performs exact semantic search: candidates are narrowed with label and `w ## Request Body -| Field | Type | Required | Description | -|----------------------|----------------------------|--------------|-----------------------------------------------------------------------------------------------------| -| `propertyName` | string | **yes** | The indexed property to search against (e.g. `"description"`) | -| `labels` | string or array of strings | **yes** | Label(s) to search within (min 1) | -| `query` | string | conditionally | Free-text query to embed. Required for managed indexes; **not allowed** for external indexes. | +| Field | Type | Required | Description | +| -------------------- | -------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `propertyName` | string | **yes** | The indexed property to search against (e.g. `"description"`) | +| `labels` | string or array of strings | **yes** | Label(s) to search within (min 1) | +| `query` | string | conditionally | Free-text query to embed. Required for managed indexes; **not allowed** for external indexes. | | `queryVector` | array of numbers | conditionally | Pre-computed query vector. Required for external indexes. Also accepted for managed indexes (bypasses server embedding). | -| `similarityFunction` | string | no | `"cosine"` or `"euclidean"`. Required when multiple indexes target the same `(label, propertyName)`. | -| `dimensions` | number | no | Disambiguates when multiple indexes match. Inferred from `queryVector.length` when `queryVector` is supplied. | -| `where` | object | no | Standard RushDB filter expression applied **before** similarity scoring. | -| `skip` | number | no | Pagination offset (default `0`) | -| `limit` | number | no | Maximum results to return (default `20`) | +| `similarityFunction` | string | no | `"cosine"` or `"euclidean"`. Required when multiple indexes target the same `(label, propertyName)`. | +| `dimensions` | number | no | Disambiguates when multiple indexes match. Inferred from `queryVector.length` when `queryVector` is supplied. | +| `where` | object | no | Standard RushDB filter expression applied **before** similarity scoring. | +| `skip` | number | no | Pagination offset (default `0`) | +| `limit` | number | no | Maximum results to return (default `20`) | --- @@ -142,7 +143,7 @@ Each result carries `__label` so you can tell the entity types apart: { "data": [ { "__id": "rec_1", "__label": "Article", "__score": 0.93, "title": "...", "body": "..." }, - { "__id": "rec_2", "__label": "Post", "__score": 0.87, "body": "..." }, + { "__id": "rec_2", "__label": "Post", "__score": 0.87, "body": "..." }, { "__id": "rec_3", "__label": "Comment", "__score": 0.82, "body": "..." } ], "success": true @@ -194,10 +195,10 @@ curl -X POST https://api.rushdb.com/api/v1/ai/search \ ## Error reference -| HTTP | Cause | -|------|-------| -| `404 Not Found` | No enabled embedding index found for `(label, propertyName)` | -| `422 Unprocessable Entity` | Multiple indexes match and `similarityFunction` was not specified | +| HTTP | Cause | +| -------------------------- | -------------------------------------------------------------------- | +| `404 Not Found` | No enabled embedding index found for `(label, propertyName)` | +| `422 Unprocessable Entity` | Multiple indexes match and `similarityFunction` was not specified | | `422 Unprocessable Entity` | `query` text supplied for an external index (server cannot embed it) | -| `422 Unprocessable Entity` | `queryVector` length does not match index `dimensions` | -| `503 Service Unavailable` | Embedding model unavailable (managed indexes only) | +| `422 Unprocessable Entity` | `queryVector` length does not match index `dimensions` | +| `503 Service Unavailable` | Embedding model unavailable (managed indexes only) | diff --git a/docs/docs/rest-api/ai/write-with-vectors.md b/docs/docs/learn/reference/rest-api/ai-and-vectors/write-with-vectors.md similarity index 95% rename from docs/docs/rest-api/ai/write-with-vectors.md rename to docs/docs/learn/reference/rest-api/ai-and-vectors/write-with-vectors.md index ea90f311..03231a7d 100644 --- a/docs/docs/rest-api/ai/write-with-vectors.md +++ b/docs/docs/learn/reference/rest-api/ai-and-vectors/write-with-vectors.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/ai/write-with-vectors sidebar_position: 4 title: Writing Records with Vectors --- @@ -7,7 +8,7 @@ title: Writing Records with Vectors RushDB lets you attach pre-computed embedding vectors to records **at write time**, eliminating the need for a separate `POST /api/v1/ai/indexes/:id/vectors/upsert` call. Any endpoint that creates or modifies records accepts a `vectors` field. -This feature requires at least one [external index](./advanced-indexing.md) to exist for the target `(label, propertyName)`. +This feature requires at least one [external index](/rest-api/ai/advanced-indexing) to exist for the target `(label, propertyName)`. --- @@ -26,7 +27,7 @@ All write endpoints accept a `vectors` array: ``` | Field | Type | Required | Description | -|----------------------|------------------|----------|-----------------------------------------------------------| +| -------------------- | ---------------- | -------- | --------------------------------------------------------- | | `propertyName` | string | **yes** | Property name this vector is associated with | | `vector` | array of numbers | **yes** | Pre-computed embedding vector | | `similarityFunction` | string | no | Required when multiple indexes exist on the same property | diff --git a/docs/docs/learn/reference/rest-api/introduction.md b/docs/docs/learn/reference/rest-api/introduction.md new file mode 100644 index 00000000..017268c9 --- /dev/null +++ b/docs/docs/learn/reference/rest-api/introduction.md @@ -0,0 +1,31 @@ +--- +slug: /rest-api/introduction +title: Introduction +sidebar_position: 0 +--- + +# REST API + +Base URL: `https://api.rushdb.com/api/v1` +Auth: `Authorization: Bearer YOUR_TOKEN` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"label":"MOVIE","data":{"title":"Inception","rating":8.8}}' +``` + +Interactive docs: [Swagger UI](https://api.rushdb.com/api) · [OpenAPI JSON](https://api.rushdb.com/api-json) + +## Endpoints + +| Group | Description | +| --------------------------------------------- | ---------------------------------------------------- | +| [Records](/rest-api/records/create-records) | Create, read, update, delete, search, import, export | +| [Relationships](/rest-api/relationships) | Attach and detach edges between records | +| [Labels](/rest-api/labels) | Query which types exist and their counts | +| [Properties](/rest-api/properties) | Inspect field names, types, and value ranges | +| [Transactions](/rest-api/transactions) | Atomic multi-step operations | +| [AI & Semantic Search](/rest-api/ai/overview) | Schema export + vector similarity search | +| [Raw Queries](/rest-api/raw-queries) | Cypher pass-through (cloud only) | diff --git a/docs/docs/rest-api/labels.md b/docs/docs/learn/reference/rest-api/labels.md similarity index 96% rename from docs/docs/rest-api/labels.md rename to docs/docs/learn/reference/rest-api/labels.md index 88a9d419..413c2d61 100644 --- a/docs/docs/rest-api/labels.md +++ b/docs/docs/learn/reference/rest-api/labels.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/labels sidebar_position: 5 --- diff --git a/docs/docs/rest-api/properties.md b/docs/docs/learn/reference/rest-api/properties.md similarity index 98% rename from docs/docs/rest-api/properties.md rename to docs/docs/learn/reference/rest-api/properties.md index e6f103c5..2958e473 100644 --- a/docs/docs/rest-api/properties.md +++ b/docs/docs/learn/reference/rest-api/properties.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/properties sidebar_position: 4 --- diff --git a/docs/docs/rest-api/raw-queries.md b/docs/docs/learn/reference/rest-api/raw-queries.md similarity index 97% rename from docs/docs/rest-api/raw-queries.md rename to docs/docs/learn/reference/rest-api/raw-queries.md index 59ff5079..0720fb32 100644 --- a/docs/docs/rest-api/raw-queries.md +++ b/docs/docs/learn/reference/rest-api/raw-queries.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/raw-queries sidebar_position: 8 --- diff --git a/docs/docs/rest-api/records/create-records.md b/docs/docs/learn/reference/rest-api/records/create-records.md similarity index 95% rename from docs/docs/rest-api/records/create-records.md rename to docs/docs/learn/reference/rest-api/records/create-records.md index b3c378d9..b7d30f4c 100644 --- a/docs/docs/rest-api/records/create-records.md +++ b/docs/docs/learn/reference/rest-api/records/create-records.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/records/create-records sidebar_position: 1 --- @@ -105,4 +106,4 @@ curl -X POST https://api.rushdb.com/api/v1/tx/$TX_ID/commit \ ## See also -- [Writing Records with Vectors](../ai/write-with-vectors.md) — attach embedding vectors when creating or upserting records +- [Writing Records with Vectors](/rest-api/ai/write-with-vectors) — attach embedding vectors when creating or upserting records diff --git a/docs/docs/rest-api/records/delete-records.md b/docs/docs/learn/reference/rest-api/records/delete-records.md similarity index 93% rename from docs/docs/rest-api/records/delete-records.md rename to docs/docs/learn/reference/rest-api/records/delete-records.md index 929ac5c0..ab1ed463 100644 --- a/docs/docs/rest-api/records/delete-records.md +++ b/docs/docs/learn/reference/rest-api/records/delete-records.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/records/delete-records sidebar_position: 7 --- diff --git a/docs/docs/rest-api/records/export-data.md b/docs/docs/learn/reference/rest-api/records/export-data.md similarity index 85% rename from docs/docs/rest-api/records/export-data.md rename to docs/docs/learn/reference/rest-api/records/export-data.md index 75e584fb..d0f5f064 100644 --- a/docs/docs/rest-api/records/export-data.md +++ b/docs/docs/learn/reference/rest-api/records/export-data.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/records/export-data sidebar_position: 8 --- @@ -10,13 +11,13 @@ Export records as CSV. Accepts the same `where` / `orderBy` / `skip` / `limit` / ### Request Body -| Field | Type | Description | -|-----------|------------------|--------------------------------------------------| -| `where` | object | Filter conditions | -| `orderBy` | string or object | Sort criteria | -| `skip` | number | Pagination offset | -| `limit` | number | Max records to return (up to 1000) | -| `labels` | array of strings | Restrict to specific labels | +| Field | Type | Description | +| --------- | ---------------- | ---------------------------------- | +| `where` | object | Filter conditions | +| `orderBy` | string or object | Sort criteria | +| `skip` | number | Pagination offset | +| `limit` | number | Max records to return (up to 1000) | +| `labels` | array of strings | Restrict to specific labels | ### Example Request diff --git a/docs/docs/rest-api/records/get-records.md b/docs/docs/learn/reference/rest-api/records/get-records.md similarity index 75% rename from docs/docs/rest-api/records/get-records.md rename to docs/docs/learn/reference/rest-api/records/get-records.md index 9a5bcfdd..8c0ceacb 100644 --- a/docs/docs/rest-api/records/get-records.md +++ b/docs/docs/learn/reference/rest-api/records/get-records.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/records/get-records sidebar_position: 5 --- @@ -31,15 +32,15 @@ curl -X POST https://api.rushdb.com/api/v1/records/search \ ### Request body -| Field | Type | Description | -| ----------- | ---------- | -------------------------------------------- | -| `labels` | `string[]` | Filter by one or more labels | -| `where` | `object` | Field conditions and operators | -| `orderBy` | `object` | `{"field": "asc" \| "desc"}` | -| `limit` | `number` | Max records. **Omit when using `select`** | -| `skip` | `number` | Records to skip (pagination) | -| `select` | `object` | Output-shaping expressions (preferred) | -| `groupBy` | `string[]` | Group aggregated results | +| Field | Type | Description | +| --------- | ---------- | ----------------------------------------- | +| `labels` | `string[]` | Filter by one or more labels | +| `where` | `object` | Field conditions and operators | +| `orderBy` | `object` | `{"field": "asc" \| "desc"}` | +| `limit` | `number` | Max records. **Omit when using `select`** | +| `skip` | `number` | Records to skip (pagination) | +| `select` | `object` | Output-shaping expressions (preferred) | +| `groupBy` | `string[]` | Group aggregated results | ### Relationship traversal diff --git a/docs/docs/rest-api/records/import-data.md b/docs/docs/learn/reference/rest-api/records/import-data.md similarity index 64% rename from docs/docs/rest-api/records/import-data.md rename to docs/docs/learn/reference/rest-api/records/import-data.md index 3f56c1d1..62d0b19d 100644 --- a/docs/docs/rest-api/records/import-data.md +++ b/docs/docs/learn/reference/rest-api/records/import-data.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/records/import-data sidebar_position: 1 --- @@ -30,15 +31,15 @@ curl -X POST https://api.rushdb.com/api/v1/records/import/json \ ### Options -| Option | Default | Description | -| ------------------------------- | ------------------------------- | -------------------------------------- | -| `suggestTypes` | `true` | Infer property types automatically | -| `convertNumericValuesToNumbers` | `false` | Convert string numbers to number type | -| `capitalizeLabels` | `false` | Uppercase all inferred label names | -| `relationshipType` | `__RUSHDB__RELATION__DEFAULT__` | Relationship type for nested links | +| Option | Default | Description | +| ------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `suggestTypes` | `true` | Infer property types automatically | +| `convertNumericValuesToNumbers` | `false` | Convert string numbers to number type | +| `capitalizeLabels` | `false` | Uppercase all inferred label names | +| `relationshipType` | `__RUSHDB__RELATION__DEFAULT__` | Relationship type for nested links | | `returnResult` | `false` | Return created records in the response. For imports exceeding 1000 records, this option is ignored and a summary object (`{ message, count }`) is returned instead. | -| `mergeBy` | — | Fields to match on for upsert | -| `mergeStrategy` | `append` | `append` or `rewrite` | +| `mergeBy` | — | Fields to match on for upsert | +| `mergeStrategy` | `append` | `append` or `rewrite` | ## Import CSV @@ -86,4 +87,4 @@ curl -X POST https://api.rushdb.com/api/v1/records/import/json \ ## See also -- [Writing Records with Vectors](../ai/write-with-vectors.md) — attach pre-computed embedding vectors to records at write time +- [Writing Records with Vectors](/rest-api/ai/write-with-vectors) — attach pre-computed embedding vectors to records at write time diff --git a/docs/docs/rest-api/records/update-records.md b/docs/docs/learn/reference/rest-api/records/update-records.md similarity index 69% rename from docs/docs/rest-api/records/update-records.md rename to docs/docs/learn/reference/rest-api/records/update-records.md index cd6b8f44..0740fab9 100644 --- a/docs/docs/rest-api/records/update-records.md +++ b/docs/docs/learn/reference/rest-api/records/update-records.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/records/update-records sidebar_position: 6 --- @@ -32,12 +33,11 @@ curl -X PUT https://api.rushdb.com/api/v1/records/movie-123 \ ## Request body -| Field | Type | Description | -|---|---|---| +| Field | Type | Description | +| ------- | -------- | ----------------------------------- | | `label` | `string` | (Optional) New label for the record | -| `data` | `object` | Properties to write | +| `data` | `object` | Properties to write | ## See also -- [Writing Records with Vectors](../ai/write-with-vectors.md) — attach embedding vectors when creating or upserting records - +- [Writing Records with Vectors](/rest-api/ai/write-with-vectors) — attach embedding vectors when creating or upserting records diff --git a/docs/docs/rest-api/relationships.md b/docs/docs/learn/reference/rest-api/relationships.md similarity index 92% rename from docs/docs/rest-api/relationships.md rename to docs/docs/learn/reference/rest-api/relationships.md index 24494bda..7985b04a 100644 --- a/docs/docs/rest-api/relationships.md +++ b/docs/docs/learn/reference/rest-api/relationships.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/relationships sidebar_position: 3 --- @@ -104,6 +105,10 @@ curl -X POST https://api.rushdb.com/api/v1/relationships/search \ -d '{"where": {"sourceRecord": {"title": "Inception"}}}' ``` +## Suggested Patterns + +RushDB can analyze a project's ontology and propose relationship patterns for review before applying them. See [Suggested Relationship Patterns](/build/graph/suggested-patterns) for the lifecycle, SDK examples, and all five REST endpoints. + ## Direction | Value | Meaning | diff --git a/docs/docs/rest-api/transactions.md b/docs/docs/learn/reference/rest-api/transactions.md similarity index 98% rename from docs/docs/rest-api/transactions.md rename to docs/docs/learn/reference/rest-api/transactions.md index 060f2b17..4c997cb0 100644 --- a/docs/docs/rest-api/transactions.md +++ b/docs/docs/learn/reference/rest-api/transactions.md @@ -1,4 +1,5 @@ --- +slug: /rest-api/transactions sidebar_position: 6 --- diff --git a/docs/docs/typescript-sdk/typescript-reference/DBRecord.md b/docs/docs/learn/reference/typescript/DBRecord.md similarity index 73% rename from docs/docs/typescript-sdk/typescript-reference/DBRecord.md rename to docs/docs/learn/reference/typescript/DBRecord.md index 8dc34fee..9272d54f 100644 --- a/docs/docs/typescript-sdk/typescript-reference/DBRecord.md +++ b/docs/docs/learn/reference/typescript/DBRecord.md @@ -1,10 +1,11 @@ --- +slug: /reference/typescript/DBRecord sidebar_position: 2 --- # DBRecord -`DBRecord` is a type representing a database [record](../../concepts/records) in RushDB. It combines internal system properties with schema-defined data [properties](../../concepts/properties). +`DBRecord` is a type representing a database [record](/build/data/store-records) in RushDB. It combines internal system properties with schema-defined data [properties](/build/schema/labels-and-properties). ## Type Definition @@ -16,8 +17,8 @@ export type DBRecord = FlattenTypes< ## Type Parameters -| Parameter | Description | -|-----------|-------------| +| Parameter | Description | +| --------------------------- | -------------------------------------------------------- | | `S extends Schema = Schema` | The schema type that defines the structure of the record | ## Properties @@ -26,10 +27,10 @@ A `DBRecord` consists of both system properties and schema-defined properties: ### System Properties -| Property | Type | Description | -|----------|------|-------------| -| `__id` | `string` | The unique identifier of the record | -| `__label` | `string` | The label/type of the record | +| Property | Type | Description | +| ------------- | -------- | ---------------------------------------- | +| `__id` | `string` | The unique identifier of the record | +| `__label` | `string` | The label/type of the record | | `__proptypes` | `object` | Property type information for the record | ### Schema Properties @@ -74,8 +75,7 @@ Contains the schema-defined properties of a database record. ### DBRecordInferred ```typescript -export type DBRecordInferred> = - DBRecord +export type DBRecordInferred> = DBRecord ``` An extension of `DBRecord` for a search query result. Aggregated fields from `select` expressions are typed based on the query shape. @@ -88,10 +88,10 @@ const UserSchema = { name: { type: 'string', required: true }, email: { type: 'string', required: true, unique: true }, age: { type: 'number' } -}; +} // A record matching this schema would have type: -type UserRecord = DBRecord; +type UserRecord = DBRecord // Example of what the record would look like: const user: UserRecord = { @@ -101,5 +101,5 @@ const user: UserRecord = { name: 'John Doe', email: 'john@example.com', age: 30 -}; +} ``` diff --git a/docs/docs/typescript-sdk/typescript-reference/DBRecordInstance.md b/docs/docs/learn/reference/typescript/DBRecordInstance.md similarity index 79% rename from docs/docs/typescript-sdk/typescript-reference/DBRecordInstance.md rename to docs/docs/learn/reference/typescript/DBRecordInstance.md index 45db51e0..03fc8ac7 100644 --- a/docs/docs/typescript-sdk/typescript-reference/DBRecordInstance.md +++ b/docs/docs/learn/reference/typescript/DBRecordInstance.md @@ -1,28 +1,26 @@ --- +slug: /reference/typescript/DBRecordInstance sidebar_position: 3 --- # DBRecordInstance -`DBRecordInstance` is a class that wraps a DBRecord and provides methods for manipulating it. This class serves as an interface for working with individual [records](../../concepts/records) in the database. It allows for updating [properties](../../concepts/properties) and managing [relationships](../../concepts/relationships). +`DBRecordInstance` is a class that wraps a DBRecord and provides methods for manipulating it. This class serves as an interface for working with individual [records](/build/data/store-records) in the database. It allows for updating [properties](/build/schema/labels-and-properties) and managing [relationships](/build/graph). ## Class Definition ```typescript -export class DBRecordInstance< - S extends Schema = Schema, - Q extends SearchQuery = SearchQuery -> { +export class DBRecordInstance = SearchQuery> { data?: DBRecordInferred } ``` ## Type Parameters -| Parameter | Description | -|-----------|-------------| -| `S extends Schema = Schema` | The schema type that defines the structure of the record | -| `Q extends SearchQuery = SearchQuery` | The search query type used to retrieve this record | +| Parameter | Description | +| ------------------------------------------- | -------------------------------------------------------- | +| `S extends Schema = Schema` | The schema type that defines the structure of the record | +| `Q extends SearchQuery = SearchQuery` | The search query type used to retrieve this record | ## Constructor @@ -34,9 +32,9 @@ Creates a new `DBRecordInstance`. ### Parameters -| Parameter | Type | Description | -|-----------|------|-------------| -| `data` | `DBRecordInferred` | Optional data for the record | +| Parameter | Type | Description | +| --------- | ------------------------ | ---------------------------- | +| `data` | `DBRecordInferred` | Optional data for the record | ## Properties @@ -131,6 +129,7 @@ async delete(transaction?: Transaction | string): Promise Deletes the record from the database. **Parameters**: + - `transaction`: Optional transaction or transaction ID **Returns**: Promise resolving to the result of the delete operation @@ -149,6 +148,7 @@ async update( Updates the record with the given data. **Parameters**: + - `data`: The data to update the record with - `transaction`: Optional transaction or transaction ID @@ -168,6 +168,7 @@ async set( Replaces the record's data with the given data. **Parameters**: + - `data`: The data to set on the record - `transaction`: Optional transaction or transaction ID @@ -188,6 +189,7 @@ async attach( Creates a relationship from this record to the target record(s). **Parameters**: + - `target`: The target record(s) to create a relationship to - `options`: Optional relationship options - `transaction`: Optional transaction or transaction ID @@ -209,6 +211,7 @@ async detach( Removes a relationship from this record to the target record(s). **Parameters**: + - `target`: The target record(s) to remove a relationship from - `options`: Optional relationship detach options - `transaction`: Optional transaction or transaction ID @@ -221,23 +224,23 @@ Removes a relationship from this record to the target record(s). ```typescript // Get a record instance from a model -const userRecord = await UserModel.findById('user_123'); +const userRecord = await UserModel.findById('user_123') // Access record data -console.log(userRecord.id); // 'user_123' -console.log(userRecord.label); // 'User' -console.log(userRecord.data?.name); // 'John Doe' +console.log(userRecord.id) // 'user_123' +console.log(userRecord.label) // 'User' +console.log(userRecord.data?.name) // 'John Doe' // Update the record await userRecord.update({ name: 'Jane Doe', age: 31 -}); +}) // Create a relationship to another record -const postRecord = await PostModel.findById('post_456'); -await userRecord.attach(postRecord, { type: 'AUTHORED' }); +const postRecord = await PostModel.findById('post_456') +await userRecord.attach(postRecord, { type: 'AUTHORED' }) // Delete the record -await userRecord.delete(); +await userRecord.delete() ``` diff --git a/docs/docs/typescript-sdk/typescript-reference/DBRecordTarget.md b/docs/docs/learn/reference/typescript/DBRecordTarget.md similarity index 56% rename from docs/docs/typescript-sdk/typescript-reference/DBRecordTarget.md rename to docs/docs/learn/reference/typescript/DBRecordTarget.md index c7a1a7f4..e7eb7081 100644 --- a/docs/docs/typescript-sdk/typescript-reference/DBRecordTarget.md +++ b/docs/docs/learn/reference/typescript/DBRecordTarget.md @@ -1,10 +1,11 @@ --- +slug: /reference/typescript/DBRecordTarget sidebar_position: 4 --- # DBRecordTarget -`DBRecordTarget` is a type that represents a target [record](../../concepts/records) for operations like set, update, attach, detach, and delete. It is commonly used when working with [relationships](../../concepts/relationships) and [property](../../concepts/properties) updates. +`DBRecordTarget` is a type that represents a target [record](/build/data/store-records) for operations like set, update, attach, detach, and delete. It is commonly used when working with [relationships](/build/graph) and [property](/build/schema/labels-and-properties) updates. ## Type Definition @@ -16,11 +17,11 @@ This union type allows for multiple ways to reference a record when performing o ## Type Options -| Type | Description | -|------|-------------| -| `DBRecord` | A record object with all its properties | +| Type | Description | +| ----------------------- | ---------------------------------------------------------- | +| `DBRecord` | A record object with all its properties | | `DBRecordInstance` | A record instance with methods for manipulating the record | -| `string` | A record ID string | +| `string` | A record ID string | ## Usage @@ -34,27 +35,27 @@ await UserModel.attach({ source: 'user_123', target: 'post_456', options: { type: 'AUTHORED' } -}); +}) // Using a record object const user = { __id: 'user_123', __label: 'User', name: 'John Doe' -}; +} await UserModel.attach({ source: user, target: 'post_456', options: { type: 'AUTHORED' } -}); +}) // Using a record instance -const userInstance = await UserModel.findById('user_123'); +const userInstance = await UserModel.findById('user_123') await UserModel.attach({ source: userInstance, target: 'post_456', options: { type: 'AUTHORED' } -}); +}) ``` ### In Record Instance Methods @@ -62,20 +63,20 @@ await UserModel.attach({ While `DBRecordInstance` methods like `attach` and `detach` use `DBRecordTarget` internally, you don't need to explicitly use this type as the instance's ID is already known: ```typescript -const userInstance = await UserModel.findById('user_123'); +const userInstance = await UserModel.findById('user_123') // Using a record ID string -await userInstance.attach('post_456', { type: 'AUTHORED' }); +await userInstance.attach('post_456', { type: 'AUTHORED' }) // Using a record object const post = { __id: 'post_456', __label: 'Post', title: 'My Post' -}; -await userInstance.attach(post, { type: 'AUTHORED' }); +} +await userInstance.attach(post, { type: 'AUTHORED' }) // Using a record instance -const postInstance = await PostModel.findById('post_456'); -await userInstance.attach(postInstance, { type: 'AUTHORED' }); +const postInstance = await PostModel.findById('post_456') +await userInstance.attach(postInstance, { type: 'AUTHORED' }) ``` diff --git a/docs/docs/typescript-sdk/typescript-reference/DBRecordsArrayInstance.md b/docs/docs/learn/reference/typescript/DBRecordsArrayInstance.md similarity index 79% rename from docs/docs/typescript-sdk/typescript-reference/DBRecordsArrayInstance.md rename to docs/docs/learn/reference/typescript/DBRecordsArrayInstance.md index ebfbed9b..d7725ed7 100644 --- a/docs/docs/typescript-sdk/typescript-reference/DBRecordsArrayInstance.md +++ b/docs/docs/learn/reference/typescript/DBRecordsArrayInstance.md @@ -1,18 +1,16 @@ --- +slug: /reference/typescript/DBRecordsArrayInstance sidebar_position: 5 --- # DBRecordsArrayInstance -`DBRecordsArrayInstance` is a class that manages an array of `DBRecordInstance` objects. It typically represents the result of a [search query](../../concepts/search/introduction.md) that returns multiple [records](../../concepts/records). +`DBRecordsArrayInstance` is a class that manages an array of `DBRecordInstance` objects. It typically represents the result of a [search query](/reference/search-query) that returns multiple [records](/build/data/store-records). ## Class Definition ```typescript -export class DBRecordsArrayInstance< - S extends Schema = Schema, - Q extends SearchQuery = SearchQuery -> { +export class DBRecordsArrayInstance = SearchQuery> { data?: Array> total: number | undefined searchQuery?: SearchQuery @@ -22,7 +20,7 @@ export class DBRecordsArrayInstance< ## Type Parameters | Parameter | Description | -|---------------------------------------------|-----------------------------------------------------------| +| ------------------------------------------- | --------------------------------------------------------- | | `S extends Schema = Schema` | The schema type that defines the structure of the records | | `Q extends SearchQuery = SearchQuery` | The search query type used to retrieve these records | @@ -41,7 +39,7 @@ Creates a new `DBRecordsArrayInstance`. ### Parameters | Parameter | Type | Description | -|---------------|---------------------------------|----------------------------------------------------------------------------------------------------| +| ------------- | ------------------------------- | -------------------------------------------------------------------------------------------------- | | `data` | `Array>` | Optional array of record instances | | `total` | `number` | Optional total count of records (may be greater than the length of `data` when pagination is used) | | `searchQuery` | `SearchQuery` | Optional search query that was used to retrieve these records | @@ -83,6 +81,7 @@ async deleteAll(transaction?: Transaction | string): Promise<{ success: boolean Deletes all records in this result set. **Parameters**: + - `transaction`: Optional transaction or transaction ID **Returns**: Promise resolving to `{ success: boolean }` @@ -96,6 +95,7 @@ async next(options?: { preserveData?: boolean }): Promise { - console.log(user.id, user.data?.name); -}); +userRecords.data?.forEach((user) => { + console.log(user.id, user.data?.name) +}) // Fetch next page -const nextPage = await userRecords.next(); +const nextPage = await userRecords.next() // Or append next page to existing results -await userRecords.next({ preserveData: true }); -console.log(userRecords.data.length); // Now contains up to 20 records +await userRecords.next({ preserveData: true }) +console.log(userRecords.data.length) // Now contains up to 20 records // Export to CSV -const csv = userRecords.exportCsv(); +const csv = userRecords.exportCsv() // Bulk update a property -await userRecords.setProperties({ active: false }); +await userRecords.setProperties({ active: false }) // Delete all found records -await userRecords.deleteAll(); +await userRecords.deleteAll() ``` diff --git a/docs/docs/typescript-sdk/typescript-reference/Model.md b/docs/docs/learn/reference/typescript/Model.md similarity index 79% rename from docs/docs/typescript-sdk/typescript-reference/Model.md rename to docs/docs/learn/reference/typescript/Model.md index 3f73dd66..66470b9c 100644 --- a/docs/docs/typescript-sdk/typescript-reference/Model.md +++ b/docs/docs/learn/reference/typescript/Model.md @@ -1,15 +1,16 @@ --- +slug: /reference/typescript/Model sidebar_position: 1 --- # Model -The `Model` class represents a schema-defined entity in RushDB. It provides methods to perform CRUD operations and manage [relationships](../../concepts/relationships) between [records](../../concepts/records). Models are identified by [labels](../../concepts/labels) in the database. +The `Model` class represents a schema-defined entity in RushDB. It provides methods to perform CRUD operations and manage [relationships](/build/graph) between [records](/build/data/store-records). Models are identified by [labels](/build/schema/labels-and-properties) in the database. ## Usage ```typescript -import { Model } from '@rushdb/javascript-sdk'; +import { Model } from '@rushdb/javascript-sdk' // Define a schema const UserSchema = { @@ -17,19 +18,19 @@ const UserSchema = { email: { type: 'string', required: true, unique: true }, age: { type: 'number' }, active: { type: 'boolean', default: true } -}; +} // Create a model -const UserModel = new Model('User', UserSchema); +const UserModel = new Model('User', UserSchema) // Or with target label validation enabled -const StrictUserModel = new Model('User', UserSchema, { validateTargetLabel: true }); +const StrictUserModel = new Model('User', UserSchema, { validateTargetLabel: true }) ``` ## Type Parameters | Parameter | Description | -|--------------------------|-------------------------------------------------------------------| +| ------------------------ | ----------------------------------------------------------------- | | `S extends Schema = any` | The schema type that defines the structure of the model's records | ## Constructor @@ -42,17 +43,17 @@ Creates a new `Model` instance. ### Parameters -| Parameter | Type | Description | -|------------------|------------------|----------------------------------------------------------------------------| -| `modelName` | `string` | The name/label of the model in the database | -| `schema` | `S` | The schema definition that describes the model's structure | -| `config` | `ModelConfig` | Optional configuration (see below) | +| Parameter | Type | Description | +| ----------- | ------------- | ---------------------------------------------------------- | +| `modelName` | `string` | The name/label of the model in the database | +| `schema` | `S` | The schema definition that describes the model's structure | +| `config` | `ModelConfig` | Optional configuration (see below) | ### ModelConfig -| Option | Type | Default | Description | -|------------------------|-----------|---------|-------------| -| `validateTargetLabel` | `boolean` | `false` | When `true`, `set()` and `update()` verify the target record's label matches this model before proceeding. Adds one extra DB read per call. | +| Option | Type | Default | Description | +| --------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `validateTargetLabel` | `boolean` | `false` | When `true`, `set()` and `update()` verify the target record's label matches this model before proceeding. Adds one extra DB read per call. | ## Properties @@ -89,13 +90,13 @@ readonly record!: DBRecord Type helper for a fully-defined record with database representation. Similar to the draft, but includes all fields that come with the record's database-side representation, such as `__id`, `__label`, and `__proptypes`. ### searchQuery + ```typescript readonly searchQuery!: SearchQuery ``` Type helper for a SearchQuery of the schema. Represents a structured query input that enables filtering, sorting, pagination, and aggregation of records based on schema-defined fields. Useful for composing reusable, type-safe search expressions. - ### recordInstance ```typescript @@ -117,6 +118,7 @@ Type helper for an array of record instances. Similar to a single record instanc These helpers are available for typing only and are not populated at runtime. They exist to let you derive precise types from a model without constructing values in code. Properties that are type-only: + - `draft` - `record` - `recordInstance` @@ -127,12 +129,13 @@ Practical usage examples: ```ts // DO: derive types using typeof -export type UserRecord = typeof UserModel.record; -export type UserRecordResult = never> = - typeof UserModel.recordInstance & { data: T }; -export type UserRecordsArrayResult = typeof UserModel.recordsArrayInstance; -export type UserRecordDraft = typeof UserModel.draft; -export type UserSearchQuery = SearchQuery; +export type UserRecord = typeof UserModel.record +export type UserRecordResult = never> = typeof UserModel.recordInstance & { + data: T +} +export type UserRecordsArrayResult = typeof UserModel.recordsArrayInstance +export type UserRecordDraft = typeof UserModel.draft +export type UserSearchQuery = SearchQuery // DON'T: read them at runtime — they aren’t real values // console.log(UserModel.record) // undefined at runtime @@ -145,6 +148,7 @@ If you need an actual instance at runtime, work with query results (e.g., `find` When you add aggregations or grouping to a SearchQuery, the result shape may no longer match your model schema. To accommodate this, the helper type uses a wildcard extension: `typeof Model.recordInstance & { data: T }`. How it works: + - `typeof Model.recordInstance` gives you the instance methods and base record metadata. - `& { data: T }` lets you “overlay” the actual data payload you expect back from an aggregated/grouped query, bypassing strict schema checks in those scenarios. @@ -157,15 +161,16 @@ const deals = await DealModel.find({ totalAmount: { $sum: '$record.amount' } }, groupBy: ['totalAmount'] -}); +}) // Tell TS what the aggregated row contains -type DealAggRow = typeof DealModel.recordInstance & { data: { totalAmount: number } }; -const firstRow = deals.data[0] as DealAggRow; +type DealAggRow = typeof DealModel.recordInstance & { data: { totalAmount: number } } +const firstRow = deals.data[0] as DealAggRow ``` Notes: -- Use this pattern whenever [`select`](../../concepts/search/select) and/or [`groupBy`](../../concepts/search/group-by) change the returned columns. + +- Use this pattern whenever [`select`](/reference/select-expressions) and/or [`groupBy`](/reference/group-by) change the returned columns. - For regular record fetches (no aggregation/grouping), prefer the plain `typeof Model.recordInstance` without augmenting `data`. ## Methods @@ -189,6 +194,7 @@ public toInstance(record: DBRecord): DBRecordInstance Converts a database record to a record instance with additional methods. **Parameters**: + - `record`: The database record to convert **Returns**: A record instance with additional methods @@ -207,6 +213,7 @@ async find = SearchQuery>( Finds records that match the given search criteria. **Parameters**: + - `searchQuery`: Optional search criteria - `transaction`: Optional transaction or transaction ID @@ -228,6 +235,7 @@ async findOne = SearchQuery>( Finds a single record that matches the given search criteria. **Parameters**: + - `searchQuery`: Optional search criteria - `transaction`: Optional transaction or transaction ID @@ -245,6 +253,7 @@ async findById( Finds a record by its ID. **Parameters**: + - `id`: The ID of the record to find - `transaction`: Optional transaction or transaction ID @@ -266,6 +275,7 @@ async findUniq = SearchQuery>( Finds a unique record that matches the given search criteria. **Parameters**: + - `searchQuery`: Optional search criteria - `transaction`: Optional transaction or transaction ID @@ -284,14 +294,15 @@ async create( Creates a new record in the database. **Parameters**: + - `record`: The record data to create - `transaction`: Optional transaction or transaction ID - `options`: Optional behavior on uniqueness conflict **`CreateOptions`**: -| Option | Type | Default | Description | -|--------------|------------------------------------|-----------|---------| +| Option | Type | Default | Description | +| ------------ | ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------ | | `onConflict` | `'throw'` \| `'upsert'` \| `'skip'` | `'throw'` | `'throw'` throws `UniquenessError`, `'upsert'` updates the conflicting record, `'skip'` returns `null` | **Returns**: Promise resolving to the created record, or `null` when `onConflict: 'skip'` and a conflict exists @@ -318,6 +329,7 @@ attach( Attaches a relationship between records. **Parameters**: + - `params`: Object containing source, target, and relationship options - `transaction`: Optional transaction or transaction ID @@ -334,6 +346,7 @@ export type InferType = Model> = FlattenTypes = never> = - typeof UserModel.recordInstance & { data: T }; -export type UserRecordsArrayResult = typeof UserModel.recordsArrayInstance; -export type UserRecordDraft = typeof UserModel.draft; -export type UserSearchQuery = SearchQuery; +export type UserRecord = typeof UserModel.record +export type UserRecordResult = never> = typeof UserModel.recordInstance & { + data: T +} +export type UserRecordsArrayResult = typeof UserModel.recordsArrayInstance +export type UserRecordDraft = typeof UserModel.draft +export type UserSearchQuery = SearchQuery // This map is used to teach the SDK about your labels and their schemas export type Models = { - [USER]: typeof UserModel.schema; -}; + [USER]: typeof UserModel.schema +} ``` Notes: + - The property for uniqueness is `unique` in the SDK. - Adjust any import aliases like `@/common/...` to match your project setup. @@ -386,10 +401,10 @@ Create a project-level declaration file (for example `index.d.ts`) that merges y ```ts // index.d.ts (path can be anything, just ensure tsconfig includes it) -import {} from '@rushdb/javascript-sdk'; +import {} from '@rushdb/javascript-sdk' // Import the map you exported in step 1 (adjust the path for your project) -import { Models as AppModels } from '@/platform-app/types'; +import { Models as AppModels } from '@/platform-app/types' declare module '@rushdb/javascript-sdk' { // This merges your labels/schemas so SearchQuery gets rich intellisense @@ -412,21 +427,22 @@ Minimal tsconfig settings to verify: "module": "esnext", "target": "es2020", "moduleResolution": "bundler", // or "node"/"node16" depending on your toolchain - "baseUrl": ".", // if you use path aliases + "baseUrl": ".", // if you use path aliases "paths": { - "@/*": ["src/*"] // adjust to match your alias usage like '@/...' - }, + "@/*": ["src/*"] // adjust to match your alias usage like '@/...' + } // Keep this true if you want to validate library .d.ts files too // "skipLibCheck": false }, "include": [ "src", - "index.d.ts" // include your augmentation (or its actual path) + "index.d.ts" // include your augmentation (or its actual path) ] } ``` Common gotchas: + - If the module augmentation isn’t picked up, double-check that the `.d.ts` file is inside tsconfig’s `include` (or not excluded). - Monorepo/workspaces: put the augmentation in the consuming app/package and ensure that package’s tsconfig includes it. - If you use ESM + TS 5’s “verbatimModuleSyntax”, keep imports as type-only where needed. @@ -440,10 +456,10 @@ const posts = await db.records.find({ labels: ['post'], where: { author: { - login: { $contains: 'john' }, // autocomplete from Author schema - }, - }, -}); + login: { $contains: 'john' } // autocomplete from Author schema + } + } +}) ``` This setup gives you end-to-end safety: drafts, records, queries, and nested relations all derive types from your single source of truth—the model schemas you define. diff --git a/docs/docs/typescript-sdk/typescript-reference/RelationTarget.md b/docs/docs/learn/reference/typescript/RelationTarget.md similarity index 65% rename from docs/docs/typescript-sdk/typescript-reference/RelationTarget.md rename to docs/docs/learn/reference/typescript/RelationTarget.md index 2166a680..250a1bbd 100644 --- a/docs/docs/typescript-sdk/typescript-reference/RelationTarget.md +++ b/docs/docs/learn/reference/typescript/RelationTarget.md @@ -1,10 +1,11 @@ --- +slug: /reference/typescript/RelationTarget sidebar_position: 6 --- # RelationTarget -`RelationTarget` is a type that represents the target(s) for [relationship](../../concepts/relationships) operations like attach and detach. +`RelationTarget` is a type that represents the target(s) for [relationship](/build/graph) operations like attach and detach. ## Type Definition @@ -20,12 +21,12 @@ This union type allows for multiple ways to reference one or more target records ## Type Options -| Type | Description | -|------|-------------| -| `DBRecordsArrayInstance` | An array instance containing multiple record instances | -| `MaybeArray>` | A single record or array of records | -| `MaybeArray>` | A single record instance or array of record instances | -| `MaybeArray` | A single record ID string or array of record ID strings | +| Type | Description | +| ----------------------------------- | ------------------------------------------------------- | +| `DBRecordsArrayInstance` | An array instance containing multiple record instances | +| `MaybeArray>` | A single record or array of records | +| `MaybeArray>` | A single record instance or array of record instances | +| `MaybeArray` | A single record ID string or array of record ID strings | Where `MaybeArray` is defined as: @@ -61,7 +62,7 @@ Specifies the direction of a relationship. ```typescript export type RelationOptions = { - direction?: RelationDirection; + direction?: RelationDirection type?: string } ``` @@ -85,60 +86,60 @@ Options for removing a relationship. ```typescript // Using a record ID string -await userInstance.attach('post_123', { type: 'AUTHORED' }); +await userInstance.attach('post_123', { type: 'AUTHORED' }) // Using a record object const post = { __id: 'post_123', __label: 'Post', title: 'My Post' -}; -await userInstance.attach(post, { type: 'AUTHORED' }); +} +await userInstance.attach(post, { type: 'AUTHORED' }) // Using a record instance -const postInstance = await PostModel.findById('post_123'); -await userInstance.attach(postInstance, { type: 'AUTHORED' }); +const postInstance = await PostModel.findById('post_123') +await userInstance.attach(postInstance, { type: 'AUTHORED' }) ``` ### Multiple Targets ```typescript // Using an array of record ID strings -await userInstance.attach(['post_123', 'post_456'], { type: 'AUTHORED' }); +await userInstance.attach(['post_123', 'post_456'], { type: 'AUTHORED' }) // Using an array of record objects const posts = [ { __id: 'post_123', __label: 'Post', title: 'First Post' }, { __id: 'post_456', __label: 'Post', title: 'Second Post' } -]; -await userInstance.attach(posts, { type: 'AUTHORED' }); +] +await userInstance.attach(posts, { type: 'AUTHORED' }) // Using an array of record instances -const firstPost = await PostModel.findById('post_123'); -const secondPost = await PostModel.findById('post_456'); -await userInstance.attach([firstPost, secondPost], { type: 'AUTHORED' }); +const firstPost = await PostModel.findById('post_123') +const secondPost = await PostModel.findById('post_456') +await userInstance.attach([firstPost, secondPost], { type: 'AUTHORED' }) // Using a DBRecordsArrayInstance from a find operation const posts = await PostModel.find({ where: { title: { $regex: '^My' } } -}); -await userInstance.attach(posts, { type: 'AUTHORED' }); +}) +await userInstance.attach(posts, { type: 'AUTHORED' }) ``` ### Detaching Relationships ```typescript // Detach all relationships to a specific record -await userInstance.detach('post_123'); +await userInstance.detach('post_123') // Detach all relationships of a specific type -await userInstance.detach('post_123', { typeOrTypes: 'AUTHORED' }); +await userInstance.detach('post_123', { typeOrTypes: 'AUTHORED' }) // Detach multiple relationship types -await userInstance.detach('post_123', { typeOrTypes: ['AUTHORED', 'LIKED'] }); +await userInstance.detach('post_123', { typeOrTypes: ['AUTHORED', 'LIKED'] }) // Detach incoming relationships only -await userInstance.detach('post_123', { direction: 'in' }); +await userInstance.detach('post_123', { direction: 'in' }) ``` diff --git a/docs/docs/typescript-sdk/typescript-reference/RushDB.md b/docs/docs/learn/reference/typescript/RushDB.md similarity index 66% rename from docs/docs/typescript-sdk/typescript-reference/RushDB.md rename to docs/docs/learn/reference/typescript/RushDB.md index 05338456..3cb0e61d 100644 --- a/docs/docs/typescript-sdk/typescript-reference/RushDB.md +++ b/docs/docs/learn/reference/typescript/RushDB.md @@ -1,21 +1,22 @@ --- +slug: /reference/typescript/RushDB sidebar_position: 7 --- # RushDB -The `RushDB` class is the main entry point for interacting with the RushDB database. It manages API connections and model registration. It provides access to [records](../../concepts/records), [labels](../../concepts/labels), and [transactions](../../concepts/transactions). +The `RushDB` class is the main entry point for interacting with the RushDB database. It manages API connections and model registration. It provides access to [records](/build/data/store-records), [labels](/build/schema/labels-and-properties), and [transactions](/build/reliability/transactions). ## Initialization ```typescript -import RushDB from '@rushdb/javascript-sdk'; +import RushDB from '@rushdb/javascript-sdk' // Create an instance with an API token const db = new RushDB('RUSHDB_API_KEY', { // Optionnaly provide API url to your RushDB instance url: 'https://api.rushdb.com/api/v1' -}); +}) ``` ## Constructor @@ -28,19 +29,19 @@ Creates a new `RushDB` instance. ### Parameters -| Parameter | Type | Description | -|-----------|------|-------------| -| `token` | `string` | Optional API token for authentication | -| `config` | `SDKConfig` | Optional configuration object | +| Parameter | Type | Description | +| --------- | ----------- | ------------------------------------- | +| `token` | `string` | Optional API token for authentication | +| `config` | `SDKConfig` | Optional configuration object | ### SDKConfig Options -| Option | Type | Description | -|--------|------|-------------| -| `url` | `string` | The base URL of the RushDB API | -| `timeout` | `number` | Request timeout in milliseconds | +| Option | Type | Description | +| ------------ | ------------ | --------------------------------- | +| `url` | `string` | The base URL of the RushDB API | +| `timeout` | `number` | Request timeout in milliseconds | | `httpClient` | `HttpClient` | Custom HTTP client implementation | -| `debug` | `boolean` | Enable debug mode | +| `debug` | `boolean` | Enable debug mode | ## Properties @@ -77,6 +78,7 @@ public toInstance(record: DBRecord): DBRecordInsta Converts a database record to a record instance. **Parameters**: + - `record`: The record to convert **Returns**: A record instance with additional methods @@ -84,29 +86,29 @@ Converts a database record to a record instance. ## Usage Example ```typescript -import RushDB, { Model } from '@rushdb/javascript-sdk'; +import RushDB, { Model } from '@rushdb/javascript-sdk' // Initialize the SDK const db = new RushDB('RUSHDB_API_KEY', { url: 'https://api.rushdb.com/api/v1' -}); +}) // Define schemas const UserSchema = { name: { type: 'string', required: true }, email: { type: 'string', required: true, unique: true }, age: { type: 'number' } -}; +} const PostSchema = { title: { type: 'string', required: true }, content: { type: 'string', required: true }, published: { type: 'boolean', default: false } -}; +} // Create models -const UserModel = new Model('User', UserSchema); -const PostModel = new Model('Post', PostSchema); +const UserModel = new Model('User', UserSchema) +const PostModel = new Model('Post', PostSchema) // Use the models to interact with the database async function main() { @@ -115,32 +117,32 @@ async function main() { name: 'John Doe', email: 'john@example.com', age: 30 - }); + }) // Create a post const post = await PostModel.create({ title: 'My First Post', content: 'This is my first post!', published: true - }); + }) // Create a relationship await UserModel.attach({ source: user, target: post, options: { type: 'AUTHORED' } - }); + }) // Find all users - const users = await UserModel.find(); - console.log(users.data?.length); + const users = await UserModel.find() + console.log(users.data?.length) // Find a specific user const foundUser = await UserModel.findOne({ where: { email: 'john@example.com' } - }); - console.log(foundUser?.data?.name); + }) + console.log(foundUser?.data?.name) } -main().catch(console.error); +main().catch(console.error) ``` diff --git a/docs/docs/typescript-sdk/typescript-reference/SearchQuery.md b/docs/docs/learn/reference/typescript/SearchQuery.md similarity index 71% rename from docs/docs/typescript-sdk/typescript-reference/SearchQuery.md rename to docs/docs/learn/reference/typescript/SearchQuery.md index 58b5d82f..f34ef627 100644 --- a/docs/docs/typescript-sdk/typescript-reference/SearchQuery.md +++ b/docs/docs/learn/reference/typescript/SearchQuery.md @@ -1,10 +1,11 @@ --- +slug: /reference/typescript/SearchQuery sidebar_position: 8 --- # SearchQuery -`SearchQuery` is a type that defines the structure for querying [records](../../concepts/records) in RushDB. It provides a flexible way to filter, sort, paginate, and compute metrics using `select` and `groupBy`. The legacy `aggregate` clause is deprecated and should only be used for vector similarity until `select` supports it. For more information on search concepts, see the [search documentation](../../concepts/search/introduction.md). +`SearchQuery` is a type that defines the structure for querying [records](/build/data/store-records) in RushDB. It provides a flexible way to filter, sort, paginate, and compute metrics using `select` and `groupBy`. The legacy `aggregate` clause is deprecated and should only be used for vector similarity until `select` supports it. For more information on search concepts, see the [search documentation](/reference/search-query). ## Type Definition @@ -19,9 +20,9 @@ export type SearchQuery = SearchQueryLabelsClause & ## Type Parameters -| Parameter | Description | -|---------------------------------|---------------------------------------------------------------------------------------------------------------| -| `S extends Schema = Schema` | The schema type that defines the structure of the records being queried. `Schema` is `Record` where `SchemaField = { type: 'boolean' \| 'datetime' \| 'null' \| 'number' \| 'string'; required?: boolean; multiple?: boolean; unique?: boolean; default?: ... }`. The default `Schema` (rather than `any`) preserves type safety while remaining permissive when no explicit schema is provided. | +| Parameter | Description | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `S extends Schema = Schema` | The schema type that defines the structure of the records being queried. `Schema` is `Record` where `SchemaField = { type: 'boolean' \| 'datetime' \| 'null' \| 'number' \| 'string'; required?: boolean; multiple?: boolean; unique?: boolean; default?: ... }`. The default `Schema` (rather than `any`) preserves type safety while remaining permissive when no explicit schema is provided. | ## Query Components @@ -47,7 +48,7 @@ export type PaginationClause = { Controls pagination of the query results. | Property | Type | Description | -|----------|----------|-------------------------------------| +| -------- | -------- | ----------------------------------- | | `limit` | `number` | Maximum number of records to return | | `skip` | `number` | Number of records to skip | @@ -79,38 +80,48 @@ Filters records based on property values and relationships. ```typescript export type CollectExpr = { // ── alias-based (requires $alias declared in where) ── - from?: string // "$alias" — the traversal alias to collect from + from?: string // "$alias" — the traversal alias to collect from // ── label-based (inline traversal, no alias needed) ── - label?: string // related record label (e.g. 'DEPARTMENT'); mutually exclusive with from - where?: Record // flat property filter on this traversal level (label-based only) + label?: string // related record label (e.g. 'DEPARTMENT'); mutually exclusive with from + where?: Record // flat property filter on this traversal level (label-based only) // ── common options ── - select?: Record // field projection; nested $collect allowed + select?: Record // field projection; nested $collect allowed orderBy?: Order - limit?: number - skip?: number - unique?: boolean // default: true + limit?: number + skip?: number + unique?: boolean // default: true } // Use $self in select to reference the current level when using label: { name: '$self.name' } // from and label are mutually exclusive. export type TimeBucketExprUnit = - | 'day' | 'week' | 'month' | 'quarter' | 'year' - | 'hour' | 'minute' | 'second' - | 'months' | 'hours' | 'minutes' | 'seconds' | 'years' + | 'day' + | 'week' + | 'month' + | 'quarter' + | 'year' + | 'hour' + | 'minute' + | 'second' + | 'months' + | 'hours' + | 'minutes' + | 'seconds' + | 'years' export type TimeBucketExpr = { - field: string // "$alias.fieldName" — must be a datetime field - unit: TimeBucketExprUnit - size?: number // required when unit is plural + field: string // "$alias.fieldName" — must be a datetime field + unit: TimeBucketExprUnit + size?: number // required when unit is plural } export type Expr = - | string // "$alias.field" or "$alias" - | number // literal - | boolean // literal - | { $ref: string } // reference another select output key + | string // "$alias.field" or "$alias" + | number // literal + | boolean // literal + | { $ref: string } // reference another select output key | { $sum: Expr } | { $avg: Expr; $precision?: number } | { $count: '*' | Expr } @@ -130,7 +141,7 @@ export type SelectClause = { } ``` -The canonical output-shaping clause. See the [Select Expressions guide](../../concepts/search/select.md) for full documentation. +The canonical output-shaping clause. See the [Select Expressions guide](/reference/select-expressions) for full documentation. ### GroupBy Clause @@ -150,41 +161,45 @@ The `where` property of a search query can include various expressions to filter ```typescript export type PropertyExpression = - BooleanExpression | - DatetimeExpression | - NullExpression | - NumberExpression | - StringExpression | - TypeExpression + | BooleanExpression + | DatetimeExpression + | NullExpression + | NumberExpression + | StringExpression + | TypeExpression ``` #### Number Expressions ```typescript -export type NumberExpression = number | { - $gt?: number - $gte?: number - $in?: Array - $lt?: number - $lte?: number - $ne?: number - $nin?: Array - $exists?: boolean -} +export type NumberExpression = + | number + | { + $gt?: number + $gte?: number + $in?: Array + $lt?: number + $lte?: number + $ne?: number + $nin?: Array + $exists?: boolean + } ``` #### String Expressions ```typescript -export type StringExpression = string | { - $in?: Array - $ne?: string - $nin?: Array - $endsWith?: string - $startsWith?: string - $contains?: string - $exists?: boolean -} +export type StringExpression = + | string + | { + $in?: Array + $ne?: string + $nin?: Array + $endsWith?: string + $startsWith?: string + $contains?: string + $exists?: boolean + } ``` #### Datetime Expressions @@ -202,30 +217,45 @@ export type DatetimeObject = { $nanosecond?: number } -export type DatetimeExpression = string | DatetimeObject | { - $gt?: DatetimeObject | string - $gte?: DatetimeObject | string - $lt?: DatetimeObject | string - $lte?: DatetimeObject | string - $ne?: DatetimeObject | string - $in?: Array - $nin?: Array - $exists?: boolean -} +export type DatetimeExpression = + | string + | DatetimeObject + | { + $gt?: DatetimeObject | string + $gte?: DatetimeObject | string + $lt?: DatetimeObject | string + $lte?: DatetimeObject | string + $ne?: DatetimeObject | string + $in?: Array + $nin?: Array + $exists?: boolean + } ``` Datetime fields support two matching styles: **ISO 8601 exact match or set membership:** + ```typescript // Exact ISO match -{ where: { created: '2023-01-01T00:00:00Z' } } +{ + where: { + created: '2023-01-01T00:00:00Z' + } +} // Match a set of dates -{ where: { created: { $in: ['2023-01-01T00:00:00Z', '2023-06-01T00:00:00Z'] } } } +{ + where: { + created: { + $in: ['2023-01-01T00:00:00Z', '2023-06-01T00:00:00Z'] + } + } +} ``` **Component object — match a specific point in time or use for range comparisons:** + ```typescript // Exact point: January 1 2023 { where: { created: { $year: 2023, $month: 1, $day: 1 } } } @@ -233,6 +263,7 @@ Datetime fields support two matching styles: :::warning Never use plain ISO strings with `$gt`/`$lt` comparisons Always use component objects for range comparisons: + ```typescript // Records created in 1994 { where: { created: { $gte: { $year: 1994 }, $lt: { $year: 1995 } } } } @@ -246,15 +277,18 @@ Always use component objects for range comparisons: // Records created on 1994-03-15 { where: { created: { $gte: { $year: 1994, $month: 3, $day: 15 }, $lt: { $year: 1994, $month: 3, $day: 16 } } } } ``` + ::: #### Boolean Expressions ```typescript -export type BooleanExpression = boolean | { - $ne?: boolean - $exists?: boolean -} +export type BooleanExpression = + | boolean + | { + $ne?: boolean + $exists?: boolean + } ``` #### Null Expressions @@ -282,7 +316,9 @@ The `$type` operator checks whether a field has a specific data type: // Find records where age is actually stored as a number { where: { - age: { $type: 'number' } + age: { + $type: 'number' + } } } ``` @@ -295,14 +331,18 @@ Filter records by their own ID directly inside the `where` clause: // Find records whose ID is in a known set { where: { - $id: { $in: ['id1', 'id2', 'id3'] } + $id: { + $in: ['id1', 'id2', 'id3'] + } } } // Filter by specific ID on a related node { where: { - EMPLOYEE: { $id: 'specific-employee-id' } + EMPLOYEE: { + $id: 'specific-employee-id' + } } } ``` @@ -372,7 +412,7 @@ Defines conditions on related records. The key of the nested object **is** the l } ``` -Learn more about [relationships in RushDB](../../concepts/relationships). +Learn more about [relationships in RushDB](/build/graph). #### $xor and $nor operators @@ -380,27 +420,21 @@ Learn more about [relationships in RushDB](../../concepts/relationships). // $xor — exactly one of the conditions must match { where: { - $xor: [ - { isPremium: true }, - { hasFreeTrialAccess: true } - ] + $xor: [{ isPremium: true }, { hasFreeTrialAccess: true }] } } // $nor — none of the conditions may match { where: { - $nor: [ - { status: 'deleted' }, - { status: 'archived' } - ] + $nor: [{ status: 'deleted' }, { status: 'archived' }] } } ``` ### Aggregation -`select` expressions support all aggregation operations. See the [Select Expressions guide](../../concepts/search/select.md) for full documentation. +`select` expressions support all aggregation operations. See the [Select Expressions guide](/reference/select-expressions) for full documentation. ```typescript // Count and sum via select expressions @@ -408,12 +442,12 @@ const stats = await db.records.find({ labels: ['COMPANY'], where: { EMPLOYEE: { $alias: '$employee', salary: { $gte: 50000 } } }, select: { - companyName: '$record.name', - headcount: { $count: '*' }, - totalWage: { $sum: '$employee.salary' }, - avgSalary: { $avg: '$employee.salary', $precision: 0 }, + companyName: '$record.name', + headcount: { $count: '*' }, + totalWage: { $sum: '$employee.salary' }, + avgSalary: { $avg: '$employee.salary', $precision: 0 } } -}); +}) ``` ## GroupBy @@ -429,12 +463,12 @@ Entries are `'$alias.propertyName'` strings. Each distinct value becomes its own const result = await db.records.find({ labels: ['DEAL'], select: { - count: { $count: '*' }, + count: { $count: '*' }, avgAmt: { $avg: '$record.amount', $precision: 2 } }, groupBy: ['$record.stage'], orderBy: { count: 'desc' } -}); +}) // Output: [{ stage: 'won', count: 42, avgAmt: 15200.00 }, ...] // Pivot on two keys (category × active) @@ -443,7 +477,7 @@ const pivot = await db.records.find({ select: { count: { $count: '*' } }, groupBy: ['$record.category', '$record.active'], orderBy: { count: 'desc' } -}); +}) ``` ### Mode B — Self-group (one row with global KPIs) @@ -456,12 +490,12 @@ const kpis = await db.records.find({ labels: ['EMPLOYEE'], select: { totalSalary: { $sum: '$record.salary' }, - headcount: { $count: '*' }, - avgSalary: { $avg: '$record.salary', $precision: 0 } + headcount: { $count: '*' }, + avgSalary: { $avg: '$record.salary', $precision: 0 } }, groupBy: ['totalSalary', 'headcount', 'avgSalary'], - orderBy: { totalSalary: 'asc' } // ← required for correct full-scan total -}); + orderBy: { totalSalary: 'asc' } // ← required for correct full-scan total +}) // Output: [{ totalSalary: 4875000, headcount: 95, avgSalary: 51315 }] ``` @@ -480,17 +514,18 @@ const wrong = await db.records.find({ labels: ['PROJECT'], select: { totalBudget: { $sum: '$record.budget' } }, groupBy: ['totalBudget'], - limit: 10 // DO NOT include -}); + limit: 10 // DO NOT include +}) // ✅ CORRECT — no limit; full dataset is summed const correct = await db.records.find({ labels: ['PROJECT'], select: { totalBudget: { $sum: '$record.budget' } }, groupBy: ['totalBudget'], - orderBy: { totalBudget: 'asc' } // triggers late ordering -}); + orderBy: { totalBudget: 'asc' } // triggers late ordering +}) ``` + ::: ## Usage Examples @@ -503,7 +538,7 @@ const users = await UserModel.find({ where: { age: { $gte: 30 } } -}); +}) ``` ### Complex Filtering @@ -515,14 +550,11 @@ const users = await UserModel.find({ $and: [ { active: true }, { - $or: [ - { email: { $endsWith: '@gmail.com' } }, - { email: { $endsWith: '@outlook.com' } } - ] + $or: [{ email: { $endsWith: '@gmail.com' } }, { email: { $endsWith: '@outlook.com' } }] } ] } -}); +}) ``` ### Pagination and Sorting @@ -533,7 +565,7 @@ const users = await UserModel.find({ orderBy: { name: 'asc' }, skip: 10, limit: 10 -}); +}) ``` ### Datetime Range Query @@ -544,7 +576,7 @@ const recent = await UserModel.find({ where: { createdAt: { $gte: { $year: 2024 }, $lt: { $year: 2025 } } } -}); +}) // Records created in Q1 2023 const q1 = await db.records.find({ @@ -552,7 +584,7 @@ const q1 = await db.records.find({ where: { issuedAt: { $gte: { $year: 2023, $month: 1 }, $lt: { $year: 2023, $month: 4 } } } -}); +}) ``` ### Filter by Record ID @@ -561,7 +593,7 @@ const q1 = await db.records.find({ // Find records from a known set of IDs const records = await db.records.find({ where: { $id: { $in: ['id1', 'id2', 'id3'] } } -}); +}) ``` ### Filtering by Related Records @@ -575,7 +607,7 @@ const users = await UserModel.find({ title: { $contains: 'Graph' } } } -}); +}) ``` ### Aggregation with `select` @@ -586,10 +618,10 @@ const stats = await db.records.find({ labels: ['COMPANY'], where: { EMPLOYEE: { $alias: '$employee', salary: { $gte: 50000 } } }, select: { - companyName: '$record.name', - headcount: { $count: '$employee' }, - totalWage: { $sum: '$employee.salary' }, - avgSalary: { $avg: '$employee.salary', $precision: 0 }, + companyName: '$record.name', + headcount: { $count: '$employee' }, + totalWage: { $sum: '$employee.salary' }, + avgSalary: { $avg: '$employee.salary', $precision: 0 }, employeeNames: { $collect: { from: '$employee', @@ -599,7 +631,7 @@ const stats = await db.records.find({ } } } -}); +}) ``` ### TimeBucket — Time-Series Aggregation (using `select`) @@ -610,34 +642,34 @@ const daily = await db.records.find({ labels: ['ORDER'], where: { issuedAt: { $gte: { $year: 2024 }, $lt: { $year: 2025 } } }, select: { - day: { $timeBucket: { field: '$record.issuedAt', unit: 'day' } }, + day: { $timeBucket: { field: '$record.issuedAt', unit: 'day' } }, count: { $count: '*' } }, groupBy: ['day'], orderBy: { day: 'asc' } -}); +}) // Monthly revenue const monthly = await db.records.find({ labels: ['ORDER'], select: { - month: { $timeBucket: { field: '$record.issuedAt', unit: 'month' } }, + month: { $timeBucket: { field: '$record.issuedAt', unit: 'month' } }, revenue: { $sum: '$record.amount' } }, groupBy: ['month'], orderBy: { month: 'asc' } -}); +}) // Bi-monthly buckets const biMonthly = await db.records.find({ labels: ['ORDER'], select: { period: { $timeBucket: { field: '$record.issuedAt', unit: 'months', size: 2 } }, - count: { $count: '*' } + count: { $count: '*' } }, groupBy: ['period'], orderBy: { period: 'asc' } -}); +}) ``` ### Nested Collect (Hierarchical Output) @@ -673,7 +705,7 @@ const tree = await db.records.find({ } } } -}); +}) // Output: [{ departments: [{ name: 'Eng', projects: [{ name: 'Platform', employees: [...] }] }] }] ``` @@ -695,5 +727,5 @@ const tree = await db.records.find({ } } } -}); +}) ``` diff --git a/docs/docs/typescript-sdk/typescript-reference/Transaction.md b/docs/docs/learn/reference/typescript/Transaction.md similarity index 73% rename from docs/docs/typescript-sdk/typescript-reference/Transaction.md rename to docs/docs/learn/reference/typescript/Transaction.md index 91ae4343..4bf3cc02 100644 --- a/docs/docs/typescript-sdk/typescript-reference/Transaction.md +++ b/docs/docs/learn/reference/typescript/Transaction.md @@ -1,3 +1,7 @@ +--- +slug: /reference/typescript/Transaction +--- + # Transaction The `Transaction` class represents an active database transaction in RushDB. It provides methods for committing or rolling back the transaction and includes the unique transaction identifier. @@ -31,10 +35,10 @@ async commit(): Promise> #### Example ```typescript -const transaction = await db.tx.begin(); +const transaction = await db.tx.begin() // Perform database operations... -const result = await transaction.commit(); -console.log(result.data.message); // "Transaction (id) has been successfully committed." +const result = await transaction.commit() +console.log(result.data.message) // "Transaction (id) has been successfully committed." ``` ### Roll Back @@ -54,14 +58,14 @@ async rollback(): Promise> #### Example ```typescript -const transaction = await db.tx.begin(); +const transaction = await db.tx.begin() // Perform database operations... try { // Some operation failed - const result = await transaction.rollback(); - console.log(result.data.message); // "Transaction (id) has been rolled back." + const result = await transaction.rollback() + console.log(result.data.message) // "Transaction (id) has been rolled back." } catch (error) { - console.error("Error rolling back transaction:", error); + console.error('Error rolling back transaction:', error) } ``` @@ -71,38 +75,38 @@ Many SDK methods accept a `Transaction` instance as their last parameter, allowi ```typescript // Start a transaction -const transaction = await db.tx.begin({ ttl: 10000 }); +const transaction = await db.tx.begin({ ttl: 10000 }) try { // Create a record within the transaction const person = await db.records.create( { - label: "Person", - data: { name: "Jane Smith", age: 28 }, + label: 'Person', + data: { name: 'Jane Smith', age: 28 } }, - transaction, - ); + transaction + ) // Update a record within the same transaction await db.records.update( { target: person, - label: "Person", - data: { age: 29 }, + label: 'Person', + data: { age: 29 } }, - transaction, - ); + transaction + ) // Commit the transaction to make changes permanent - await transaction.commit(); + await transaction.commit() } catch (error) { // If any operation fails, roll back the entire transaction - await transaction.rollback(); - throw error; + await transaction.rollback() + throw error } ``` ## See Also -- [Transactions API Documentation](../transactions) -- [RushDB Class Reference](../typescript-reference/RushDB) +- [Transactions API Documentation](/build/reliability/transactions) +- [RushDB Class Reference](/reference/typescript/RushDB) diff --git a/docs/docs/learn/reference/typescript/index.md b/docs/docs/learn/reference/typescript/index.md new file mode 100644 index 00000000..927e13ef --- /dev/null +++ b/docs/docs/learn/reference/typescript/index.md @@ -0,0 +1,58 @@ +--- +slug: /reference/typescript/ +sidebar_position: 0 +title: TypeScript / JavaScript SDK +--- + +# TypeScript / JavaScript SDK + +The official TypeScript/JavaScript SDK for RushDB. Works in Node.js and browser environments. + +## Installation + +```bash +npm install @rushdb/javascript-sdk +``` + +## Quick Start + +```typescript +import RushDB from '@rushdb/javascript-sdk' + +const db = new RushDB('RUSHDB_API_KEY') + +// Store a record +await db.records.create({ + label: 'User', + data: { name: 'Alice', role: 'engineer' } +}) + +// Query records +const { data } = await db.records.find({ + labels: ['User'], + where: { role: 'engineer' }, + limit: 10 +}) +``` + +To connect to a self-hosted instance pass the `url` option: + +```typescript +const db = new RushDB('RUSHDB_API_KEY', { + url: 'https://your-rushdb-instance.com/api/v1' +}) +``` + +## API Reference + +| Class | Description | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [RushDB](/reference/typescript/RushDB) | Main client — entry point for all operations | +| [DBRecord](/reference/typescript/DBRecord) | Typed record class returned from queries | +| [DBRecordInstance](/reference/typescript/DBRecordInstance) | Active record with instance methods (`attach`, `detach`, `update`, `delete`) | +| [DBRecordsArrayInstance](/reference/typescript/DBRecordsArrayInstance) | Collection of records with bulk-operation methods | +| [Model](/reference/typescript/Model) | Schema-bound model class for label-scoped operations | +| [Transaction](/reference/typescript/Transaction) | ACID transaction handle | +| [SearchQuery](/reference/typescript/SearchQuery) | Query builder types and interfaces | +| [DBRecordTarget](/reference/typescript/DBRecordTarget) | Record reference type used in relationship calls | +| [RelationTarget](/reference/typescript/RelationTarget) | Relationship target reference type | diff --git a/docs/docs/learn/relationships/bulk-relationships.mdx b/docs/docs/learn/relationships/bulk-relationships.mdx new file mode 100644 index 00000000..1bdb588b --- /dev/null +++ b/docs/docs/learn/relationships/bulk-relationships.mdx @@ -0,0 +1,227 @@ +--- +slug: /build/graph/bulk-relationships +sidebar_position: 2 +title: Bulk Relationships +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Bulk Relationships + +Connect or disconnect many records at once by matching on property keys. Useful after importing flat data from CSV, MongoDB exports, or external APIs where related records arrive separately with reference fields. + +--- + +## Bulk Create by Key Match + +Connect records by matching a property on the source label to a property on the target label. All pairs where the values are equal are linked. + + + + +`db.relationships.create_many()` + +```python +# MOVIE.directorId == DIRECTOR.id → create DIRECTED_BY +db.relationships.create_many( + source={"label": "MOVIE", "key": "directorId"}, + target={"label": "DIRECTOR", "key": "id"}, + type="DIRECTED_BY", + direction="out" +) +# Creates: (MOVIE) -[:DIRECTED_BY]-> (DIRECTOR) where MOVIE.directorId = DIRECTOR.id +``` + + + + +`db.relationships.createMany()` + +```typescript +await db.relationships.createMany({ + source: { label: 'MOVIE', key: 'directorId' }, + target: { label: 'DIRECTOR', key: 'id' }, + type: 'DIRECTED_BY', + direction: 'out' +}) +// Creates: MOVIE -[:DIRECTED_BY]-> DIRECTOR where MOVIE.directorId = DIRECTOR.id +``` + + + + +`POST /api/v1/relationships/create-many` + +```bash +curl -X POST https://api.rushdb.com/api/v1/relationships/create-many \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source": {"label": "MOVIE", "key": "directorId"}, + "target": {"label": "DIRECTOR", "key": "id"}, + "type": "DIRECTED_BY", + "direction": "out" + }' +``` + + + + +### Parameters + +| Field | Type | Description | +| ------------ | ----------------------- | ---------------------------------------------------------------------------------- | +| `source` | `{label, key?, where?}` | Source selector — label + key for equality join, or `where` for filter-based match | +| `target` | `{label, key?, where?}` | Target selector — same | +| `type` | `string` | Relationship type to create | +| `direction` | `"in" \| "out"` | Direction from source to target | +| `manyToMany` | `boolean` | Cartesian product mode (see below) | + +--- + +## Bulk Delete by Key Match + +Same shape as `createMany`. Removes relationships where the key values match. + + + + +```python +db.relationships.delete_many( + source={"label": "MOVIE", "key": "directorId"}, + target={"label": "DIRECTOR", "key": "id"}, + type="DIRECTED_BY", + direction="out" +) +``` + + + + +`db.relationships.deleteMany()` + +```typescript +await db.relationships.deleteMany({ + source: { label: 'MOVIE', key: 'directorId' }, + target: { label: 'DIRECTOR', key: 'id' }, + type: 'DIRECTED_BY', + direction: 'out' +}) +``` + + + + +`POST /api/v1/relationships/delete-many` + +```bash +curl -X POST https://api.rushdb.com/api/v1/relationships/delete-many \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source": {"label": "MOVIE", "key": "directorId"}, + "target": {"label": "DIRECTOR", "key": "id"}, + "type": "DIRECTED_BY" + }' +``` + + + + +--- + +## Many-to-Many (Cartesian) + +Connect or disconnect **all** records matching source conditions with **all** records matching target conditions. + +:::warning +This mode creates a cartesian product — all source × all target pairs are linked. It is **opt-in** and requires non-empty `where` filters on **both** sides to prevent accidental unbounded operations. +::: + + + + +```python +# Tag all sci-fi movies with all genre tags +db.relationships.create_many( + source={"label": "MOVIE", "where": {"genre": "sci-fi"}}, + target={"label": "TAG", "where": {"category": "genre"}}, + type="HAS_TAG", + many_to_many=True +) +``` + +Delete example: + +```python +# Remove HAS_TAG between all sci-fi movies and all genre tags +db.relationships.delete_many( + source={"label": "MOVIE", "where": {"genre": "sci-fi"}}, + target={"label": "TAG", "where": {"category": "genre"}}, + type="HAS_TAG", + many_to_many=True +) +``` + + + + +```typescript +// Create +await db.relationships.createMany({ + source: { label: 'MOVIE', where: { genre: 'sci-fi' } }, + target: { label: 'TAG', where: { category: 'genre' } }, + type: 'HAS_TAG', + manyToMany: true +}) + +// Delete +await db.relationships.deleteMany({ + source: { label: 'MOVIE', where: { genre: 'sci-fi' } }, + target: { label: 'TAG', where: { category: 'genre' } }, + type: 'HAS_TAG', + manyToMany: true +}) +``` + + + + +```bash +# Create +curl -X POST https://api.rushdb.com/api/v1/relationships/create-many \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source": {"label": "MOVIE", "where": {"genre": "sci-fi"}}, + "target": {"label": "TAG", "where": {"category": "genre"}}, + "type": "HAS_TAG", + "manyToMany": true + }' + +# Delete +curl -X POST https://api.rushdb.com/api/v1/relationships/delete-many \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source": {"label": "MOVIE", "where": {"genre": "sci-fi"}}, + "target": {"label": "TAG", "where": {"category": "genre"}}, + "type": "HAS_TAG", + "manyToMany": true + }' +``` + + + + +:::warning Both `source.where` and `target.where` must be non-empty when `manyToMany: true`. +::: + +--- + +## See also + +- [Connect Records](/build/graph/connect-records) — attach / detach individual record pairs +- [Import Data](/build/data/import-data) — nested JSON auto-creates relationships on import +- [Find & Query — Relationship Traversal](/build/data/find-and-query#relationship-traversal) — filter records via graph edges diff --git a/docs/docs/learn/relationships/connect-records.mdx b/docs/docs/learn/relationships/connect-records.mdx new file mode 100644 index 00000000..d850515c --- /dev/null +++ b/docs/docs/learn/relationships/connect-records.mdx @@ -0,0 +1,621 @@ +--- +slug: /build/graph/connect-records +sidebar_position: 1 +title: Connect Records +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Connect Records + +Relationships are the connections that link records together, creating a graph structure. They have a **type** (a string label you define) and a **direction**. + +RushDB supports three categories of relationships: + +| Type | Created by | Description | +| -------------------------------------------- | ---------------- | --------------------------------------------------- | +| `__RUSHDB__RELATION__DEFAULT__` | Auto, on import | Links parent → child records in nested JSON | +| `__RUSHDB__RELATION__VALUE__` | Auto, internally | Connects property nodes to record nodes (read-only) | +| Custom (`"STARS_IN"`, `"DIRECTED_BY"`, etc.) | You | Domain-specific, manually created | + +```mermaid +graph LR + MOVIE["MOVIE
Inception"] + ACTOR["ACTOR
Leonardo DiCaprio"] + DIRECTOR["DIRECTOR
Christopher Nolan"] + MOVIE -->|"STARS_IN"| ACTOR + MOVIE -->|"DIRECTED_BY"| DIRECTOR +``` + +--- + +## Attach Records + + + + +`db.records.attach()` + +```python +# Single target +db.records.attach( + source=movie, + target=actor, + options={"type": "STARS_IN", "direction": "out"} +) + +# One-to-many (target list) +db.records.attach( + source=movie, + target=[actor1, actor2, actor3], + options={"type": "STARS_IN"} +) +``` + + + + +`db.records.attach()` + +```typescript +// Single target +await db.records.attach({ + source: 'movie-id-123', + target: 'actor-id-456', + options: { type: 'STARS_IN', direction: 'out' } + // (MOVIE) -[:STARS_IN]-> (ACTOR) +}) + +// Multiple targets +await db.records.attach({ + source: 'movie-id-123', + target: ['actor-id-1', 'actor-id-2'], + options: { type: 'STARS_IN' } +}) +``` + +`target` can be a single ID, an array of IDs, or a record instance. + + + + +`POST /api/v1/records/:entityId/relationships` + +```bash +# Single target +curl -X POST https://api.rushdb.com/api/v1/records/$MOVIE_ID/relationships \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"targetIds": "$ACTOR_ID", "type": "STARS_IN", "direction": "out"}' + +# Multiple targets +curl -X POST https://api.rushdb.com/api/v1/records/$MOVIE_ID/relationships \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"targetIds": ["$ACTOR1_ID", "$ACTOR2_ID"], "type": "STARS_IN"}' +``` + +| Field | Type | Description | +| ----------- | -------------------- | ------------------------------------------------- | +| `targetIds` | `string \| string[]` | Target record ID(s) | +| `type` | `string` | Relationship type | +| `direction` | `"in" \| "out"` | Direction from source to target (`out` = default) | + + + + +```mermaid +graph LR + MOVIE["MOVIE
Inception"] + A1["ACTOR
Leonardo DiCaprio"] + A2["ACTOR
Cillian Murphy"] + A3["ACTOR
Ken Watanabe"] + MOVIE -->|"STARS_IN"| A1 + MOVIE -->|"STARS_IN"| A2 + MOVIE -->|"STARS_IN"| A3 +``` + +--- + +## Detach Records + + + + +`db.records.detach()` + +```python +db.records.detach( + source=movie, + target=actor, + options={"type": "STARS_IN"} + # Omit "type" to detach all relationship types +) +``` + + + + +`db.records.detach()` + +```typescript +await db.records.detach({ + source: 'movie-id-123', + target: 'actor-id-456', + options: { typeOrTypes: 'STARS_IN' } // omit to detach all types +}) +``` + + + + +`PUT /api/v1/records/:entityId/relationships` + +```bash +curl -X PUT https://api.rushdb.com/api/v1/records/$MOVIE_ID/relationships \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"targetIds": "$ACTOR_ID", "typeOrTypes": ["STARS_IN"]}' +``` + + + + +```mermaid +graph LR + MOVIE["MOVIE
Inception"] + ACTOR["ACTOR
Leonardo DiCaprio"] + MOVIE -. "❌ STARS_IN" .-> ACTOR +``` + +--- + +## List Relationships + + + + +`db.relationships.find()` + +```python +# All relationships for records matching a search +relationships = db.relationships.find( + search_query={ + "labels": ["MOVIE"], + "where": { + "ACTOR": {"$relation": "STARS_IN", "country": "USA"} + } + } +) + +# With pagination +page = db.relationships.find( + search_query={"labels": ["MOVIE"]}, + pagination={"limit": 50, "skip": 0} +) +``` + + + + +`db.relationships.find()` + +```typescript +const { data, total } = await db.relationships.find({ + labels: ['MOVIE'], + where: { + ACTOR: { $relation: 'STARS_IN', country: 'USA' } + } +}) +``` + + + + +`GET /api/v1/records/:entityId/relationships` + +```bash +curl "https://api.rushdb.com/api/v1/records/$MOVIE_ID/relationships?limit=50" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + +Search relationships across all records: + +`POST /api/v1/relationships/search` + +```bash +curl -X POST https://api.rushdb.com/api/v1/relationships/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"where": {"sourceRecord": {"title": "Inception"}}}' +``` + + + + +--- + +## Direction Reference + +| `direction` | Graph pattern | +| ----------- | ------------------------------ | +| `out` | `(source) -[:TYPE]-> (target)` | +| `in` | `(source) <-[:TYPE]- (target)` | + +```mermaid +graph LR + S1["source"] -->|"TYPE · out"| T1["target"] + T2["target"] -->|"TYPE · in"| S2["source"] +``` + +--- + +## Search by Relationship + +Use `$relation` inside a `where` clause to filter records through their graph connections. The **nested key** is the label of the related record; `$relation` constrains which relationship edge to follow. + +### Any relationship type + +Omit `$relation` to traverse any edge connecting the two labels: + + + + +```python +# Find MOVIEs connected to an ACTOR named "Timothée Chalamet" — any relationship type +result = db.records.find({ + "labels": ["MOVIE"], + "where": { + "ACTOR": { + "name": "Timothée Chalamet" + } + } +}) +``` + + + + +```typescript +// Find MOVIEs connected to an ACTOR named "Timothée Chalamet" — any relationship type +const { data } = await db.records.find({ + labels: ['MOVIE'], + where: { + ACTOR: { + name: 'Timothée Chalamet' + } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "where": { + "ACTOR": { "name": "Timothée Chalamet" } + } + }' +``` + + + + +### Filter by relationship type + +Pass `$relation` as a string to restrict traversal to a specific relationship type. This produces a direction-agnostic Cypher pattern (`-[:TYPE]-`) that matches the edge regardless of its stored direction — use the full object form (below) to also constrain direction: + + + + +```python +result = db.records.find({ + "labels": ["MOVIE"], + "where": { + "ACTOR": { + "$relation": "STARS_IN", + "country": "USA" + } + } +}) +``` + + + + +```typescript +const { data } = await db.records.find({ + labels: ['MOVIE'], + where: { + ACTOR: { + $relation: 'STARS_IN', + country: 'USA' + } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "where": { + "ACTOR": { "$relation": "STARS_IN", "country": "USA" } + } + }' +``` + + + + +### Filter by relationship type and direction + +Use the full object form when direction matters: + +| Field | Type | Description | +| ----------- | --------------- | ------------------------------------------ | +| `type` | `string` | Relationship type label | +| `direction` | `"in" \| "out"` | Edge direction relative to the root record | + + + + +```python +# Find MOVIEs where an ACTOR is connected via an incoming STARS_IN edge +# i.e. (ACTOR) -[:STARS_IN]-> (MOVIE) +result = db.records.find({ + "labels": ["MOVIE"], + "where": { + "ACTOR": { + "$relation": {"type": "STARS_IN", "direction": "in"}, + "country": "USA" + } + } +}) +``` + + + + +```typescript +// (ACTOR) -[:STARS_IN]-> (MOVIE) — direction "in" from MOVIE's perspective +const { data } = await db.records.find({ + labels: ['MOVIE'], + where: { + ACTOR: { + $relation: { type: 'STARS_IN', direction: 'in' }, + country: 'USA' + } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["MOVIE"], + "where": { + "ACTOR": { + "$relation": {"type": "STARS_IN", "direction": "in"}, + "country": "USA" + } + } + }' +``` + + + + +### Multi-hop traversal + +Nest `$relation` filters to traverse multiple relationship levels in a single query: + + + + +```python +# Companies → Engineering departments → projects with budget over 10k +result = db.records.find({ + "labels": ["COMPANY"], + "where": { + "name": "Acme Corp", + "DEPARTMENT": { + "$relation": "HAS_DEPARTMENT", + "name": "Engineering", + "PROJECT": { + "$relation": "HAS_PROJECT", + "budget": {"$gte": 10000} + } + } + } +}) +``` + + + + +```typescript +const { data } = await db.records.find({ + labels: ['COMPANY'], + where: { + name: 'Acme Corp', + DEPARTMENT: { + $relation: 'HAS_DEPARTMENT', + name: 'Engineering', + PROJECT: { + $relation: 'HAS_PROJECT', + budget: { $gte: 10000 } + } + } + } +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["COMPANY"], + "where": { + "name": "Acme Corp", + "DEPARTMENT": { + "$relation": "HAS_DEPARTMENT", + "name": "Engineering", + "PROJECT": { + "$relation": "HAS_PROJECT", + "budget": {"$gte": 10000} + } + } + } + }' +``` + + + + +```mermaid +graph LR + COMPANY["COMPANY
name: Acme Corp"] + DEPARTMENT["DEPARTMENT
name: Engineering"] + PROJECT["PROJECT
budget ≥ 10000"] + + COMPANY -->|"HAS_DEPARTMENT"| DEPARTMENT + DEPARTMENT -->|"HAS_PROJECT"| PROJECT +``` + +→ For the full operator reference including `$alias`, `$id`, and logical grouping, see [Where Operators — Relationship Queries](/reference/where-operators#relationship-queries). + +--- + +## In a Transaction + + + + +```python +tx = db.tx.begin() +try: + movie = db.records.create( + label="MOVIE", data={"title": "Dune"}, transaction=tx + ) + actor = db.records.create( + label="ACTOR", data={"name": "Timothée Chalamet"}, transaction=tx + ) + db.records.attach( + source=movie, + target=actor, + options={"type": "STARS_IN"}, + transaction=tx + ) + tx.commit() +except Exception: + tx.rollback() + raise +``` + + + + +```typescript +const tx = await db.tx.begin() +try { + const movie = await db.records.create({ label: 'MOVIE', data: { title: 'Dune' } }, tx) + const actor = await db.records.create({ label: 'ACTOR', data: { name: 'Timothée Chalamet' } }, tx) + await db.records.attach({ source: movie, target: actor, options: { type: 'STARS_IN' } }, tx) + await tx.commit() +} catch (e) { + await tx.rollback() + throw e +} +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/$MOVIE_ID/relationships \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "X-Transaction-Id: $TX_ID" \ + -d '{"targetIds": "$ACTOR_ID", "type": "STARS_IN"}' +``` + + + + +--- + +## Via Record Instance + + + + +```python +# Record instance methods — equivalent to db.records.attach/detach +movie.attach( + target=actor, + options={"type": "STARS_IN", "direction": "out"} +) + +movie.detach( + target=actor, + options={"type": "STARS_IN"} +) +``` + + + + +```typescript +// DBRecordInstance methods — equivalent to db.records.attach/detach +await movie.attach(actor, { type: 'STARS_IN', direction: 'out' }) + +await movie.detach(actor, { typeOrTypes: 'STARS_IN' }) +``` + +For TypeScript Models: + +```typescript +await MovieModel.attach({ + source: 'movie-id-123', + target: 'actor-id-456', + options: { type: 'STARS_IN', direction: 'out' } +}) + +await MovieModel.detach({ + source: 'movie-id-123', + target: 'actor-id-456', + options: { typeOrTypes: 'STARS_IN' } +}) +``` + + + + +--- + +## See also + +- [Bulk Relationships](/build/graph/bulk-relationships) — connect/disconnect many records at once by key match or many-to-many +- [Import Data](/build/data/import-data) — nested JSON auto-creates relationships +- [Transactions](/build/reliability/transactions) — ACID guarantees for multi-step operations diff --git a/docs/docs/learn/relationships/index.mdx b/docs/docs/learn/relationships/index.mdx new file mode 100644 index 00000000..d33830c9 --- /dev/null +++ b/docs/docs/learn/relationships/index.mdx @@ -0,0 +1,38 @@ +--- +slug: /build/graph/ +sidebar_position: 0 +title: Relationships +--- + +# Relationships + +Relationships connect RushDB records into a traversable graph. They are named, directed edges between records, not foreign-key columns or application-side joins. + +You can create relationships explicitly, generate them automatically from nested JSON imports, connect large datasets by matching keys, and filter records by traversing one or more hops inside a `where` clause. + +## Choose a Workflow + +| Goal | Guide | +| ----------------------------------------------------- | --------------------------------------------------------------------------- | +| Attach or detach individual record pairs | [Connect Records](/build/graph/connect-records) | +| Connect many records by matching keys | [Bulk Relationships](/build/graph/bulk-relationships) | +| Review and approve inferred graph structure | [Suggested Patterns](/build/graph/suggested-patterns) | +| Create parent-child edges while importing nested JSON | [Import Data](/build/data/import-data) | +| Filter through related records | [Relationship Traversal](/build/data/find-and-query#relationship-traversal) | + +## Relationship Sources + +| Source | What it creates | +| ------------------------------ | ------------------------------------------------------------------------------- | +| Nested JSON import | Default parent-to-child edges as RushDB decomposes the payload | +| `attach` / `detach` | Explicit edges between known record IDs | +| Bulk relationship APIs | Edges between matching source and target records | +| Approved relationship patterns | Explicit edges inferred from ontology structure and reviewed before application | + +Relationship types and direction are optional when traversing broadly and explicit when your query needs a specific edge contract. + +## Related Sections + +- [Records & Queries](/build/data/) — store and retrieve the records relationships connect +- [Labeled Meta Property Graph](/build/data/labeled-meta-property-graph) — understand record, property, and relationship layers together +- [SearchQuery Reference](/reference/search-query) — full traversal syntax diff --git a/docs/docs/learn/relationships/suggested-patterns.mdx b/docs/docs/learn/relationships/suggested-patterns.mdx new file mode 100644 index 00000000..3dd1e2d0 --- /dev/null +++ b/docs/docs/learn/relationships/suggested-patterns.mdx @@ -0,0 +1,164 @@ +--- +slug: /build/graph/suggested-patterns +sidebar_position: 3 +title: Suggested Patterns +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Suggested Relationship Patterns + +RushDB can analyze the labels, properties, and existing edges in a project and suggest relationship patterns worth making explicit. Review suggestions before applying them: analysis proposes graph structure, but approval is the step that changes relationships. + +This is useful after imports and schema evolution, when matching keys or default import edges reveal a stable domain relationship such as `ORDER -[:PLACED_BY]-> CUSTOMER`. + +## Prerequisites + +Relationship analysis uses the LLM configured by your RushDB server: + +```bash +RUSHDB_LLM_API_KEY=... +RUSHDB_LLM_MODEL=gpt-4.1-mini +``` + +For self-hosted deployments, see [Environment Variables](/deploy/config-env-variables). Omit these variables to disable AI relationship suggestions. + +## Lifecycle + +1. RushDB analyzes the current project ontology and stores inferred patterns as `suggested`. +2. Review the source, target, type, direction, mode, confidence, and rationale. +3. Approve a suggestion to apply it, or ignore it without changing relationships. +4. Delete a saved pattern when it is no longer useful. Deleting with `deleteExisting=true` also removes relationships materialized by that pattern. + +| Status | Meaning | +| ----------- | ---------------------------------------- | +| `suggested` | Awaiting review | +| `approved` | Accepted and applied | +| `ignored` | Dismissed without applying | +| `error` | Applying or analyzing the pattern failed | + +## Modes + +| Mode | What approval does | +| ------------------------------ | ------------------------------------------------------------------------ | +| `join_pattern` | Creates relationships by matching keys between source and target records | +| `retype_existing_relationship` | Replaces default import edges with an explicit domain relationship type | + +## SDK Workflow + + + + +```python +# Queue analysis, then poll list() until analysis.status returns to "idle" +client.relationships.patterns.analyze() +result = client.relationships.patterns.list() + +for pattern in result.data["patterns"]: + print(pattern["id"], pattern["type"], pattern["confidence"]) + +client.relationships.patterns.approve(pattern_id) +client.relationships.patterns.ignore(pattern_id) + +# Destructive: also removes relationships materialized by this pattern +client.relationships.patterns.delete(pattern_id, delete_existing=True) +``` + + + + +```typescript +// Queue analysis, then poll list() until analysis.status returns to "idle" +await db.relationships.patterns.analyze() +const { data } = await db.relationships.patterns.list() + +for (const pattern of data.patterns) { + console.log(pattern.id, pattern.type, pattern.confidence) +} + +await db.relationships.patterns.approve(patternId) +await db.relationships.patterns.ignore(patternId) + +// Destructive: also removes relationships materialized by this pattern +await db.relationships.patterns.delete(patternId, { deleteExisting: true }) +``` + + + + +## REST API + +All endpoints require `Authorization: Bearer ` and `x-project-id`. + +### List patterns + +```bash +curl https://api.rushdb.com/api/v1/relationships/patterns \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "x-project-id: $RUSHDB_PROJECT_ID" +``` + +The response includes `patterns`, current ontology `relationships`, and the latest `analysis` status. + +### Queue analysis + +```bash +curl -X POST https://api.rushdb.com/api/v1/relationships/patterns/analyze \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "x-project-id: $RUSHDB_PROJECT_ID" \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +The server returns `202 Accepted` with `{"queued": true}`. Poll the list endpoint for completion. + +### Approve a pattern + +```bash +curl -X POST https://api.rushdb.com/api/v1/relationships/patterns/$PATTERN_ID/approve \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "x-project-id: $RUSHDB_PROJECT_ID" \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +### Ignore a pattern + +```bash +curl -X POST https://api.rushdb.com/api/v1/relationships/patterns/$PATTERN_ID/ignore \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "x-project-id: $RUSHDB_PROJECT_ID" \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +### Delete a pattern + +```bash +# Delete the saved pattern only +curl -X DELETE https://api.rushdb.com/api/v1/relationships/patterns/$PATTERN_ID \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "x-project-id: $RUSHDB_PROJECT_ID" + +# Also remove relationships materialized by the pattern +curl -X DELETE "https://api.rushdb.com/api/v1/relationships/patterns/$PATTERN_ID?deleteExisting=true" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "x-project-id: $RUSHDB_PROJECT_ID" +``` + +:::warning +`deleteExisting=true` removes graph relationships as well as the stored pattern. Confirm this destructive action before using it. +::: + +## MCP Tools + +| Tool | Purpose | +| ----------------------------- | ---------------------------------------------------------- | +| `listRelationshipPatterns` | List patterns, ontology relationships, and analysis status | +| `analyzeRelationshipPatterns` | Queue ontology analysis | +| `approveRelationshipPattern` | Approve and apply a suggestion by ID | +| `ignoreRelationshipPattern` | Ignore a suggestion by ID | +| `deleteRelationshipPattern` | Delete a pattern, optionally with `deleteExisting` | + +Agents should call `listRelationshipPatterns` before mutating a pattern so the inferred graph change is visible for review. diff --git a/docs/docs/concepts/search/group-by.md b/docs/docs/learn/search-query/group-by.md similarity index 84% rename from docs/docs/concepts/search/group-by.md rename to docs/docs/learn/search-query/group-by.md index 29461d6f..58561ee4 100644 --- a/docs/docs/concepts/search/group-by.md +++ b/docs/docs/learn/search-query/group-by.md @@ -1,8 +1,9 @@ --- +slug: /reference/group-by sidebar_position: 8 --- -# Grouping Search Results (`groupBy`) +# Grouping Search Results The `groupBy` option in a SearchQuery lets you pivot, summarize, and compute metrics on records instead of returning a raw list. It works together with the `select` clause. If no select expressions are provided, `groupBy` is ignored. The legacy `aggregate` clause is deprecated and should only be used for vector similarity until select supports it. @@ -32,10 +33,11 @@ The `groupBy` option in a SearchQuery lets you pivot, summarize, and compute met ``` Result (conceptual): + ```json [ { "status": "pending", "count": 120, "avgTotal": 310.42 }, - { "status": "completed", "count": 85, "avgTotal": 512.10 } + { "status": "completed", "count": 85, "avgTotal": 512.1 } ] ``` @@ -92,11 +94,13 @@ Example – total deal amount across all deals: ``` Result shape (conceptual): + ```json [{ "totalAmount": 123456.78 }] ``` Notes: + - You still get an array (with one row) for consistency. - If the aggregation key appears in `groupBy`, it is not redundantly projected again as a normal field – it serves as both key and value. - Add more aggregation keys the same way if you want multiple single-value metrics (each must be repeated in `groupBy`). @@ -115,16 +119,19 @@ Multiple metrics example: ``` Conceptual result: + ```json [{ "totalRevenue": 987654.32, "orderCount": 420 }] ``` When to use: + - KPI endpoints needing a compact payload - Dashboards showing headline numbers - Reducing post-processing (no need to pluck values out of nested objects) Avoid if: + - You actually need per-dimension breakdown (then group by a property instead) - The metric might be extremely large cardinality (self-group is for single or tiny row counts) @@ -170,27 +177,34 @@ Each row: `{ name: , employees: [...], employeeCount: N }`. - `count` without `field` counts distinct records for the specified alias. - To count field occurrences: ```typescript - { $count: '$employee.country' } + { + $count: '$employee.country' + } ``` ## Ordering Grouped Results You can sort by aggregated fields or by grouped keys (since they appear in output rows): + ```typescript -orderBy: { count: 'desc' } +orderBy: { + count: 'desc' +} // or -orderBy: { dealstage: 'asc' } +orderBy: { + dealstage: 'asc' +} ``` ## Common Patterns -| Scenario | Approach | -|-------------------------------------------|----------| -| KPI dashboard by status | Group by `$record.status` + `select` count/avg expressions | -| Department summary with project list | Group by department name + `collect` project names | -| Sales funnel by stage | Group by `$record.dealstage` with count & sum | -| Pivot by category + active flag | Two keys in `groupBy` | -| Group by related location (appointments) | Alias nested traversal then group by that alias field | +| Scenario | Approach | +| ---------------------------------------- | ---------------------------------------------------------- | +| KPI dashboard by status | Group by `$record.status` + `select` count/avg expressions | +| Department summary with project list | Group by department name + `collect` project names | +| Sales funnel by stage | Group by `$record.dealstage` with count & sum | +| Pivot by category + active flag | Two keys in `groupBy` | +| Group by related location (appointments) | Alias nested traversal then group by that alias field | ## Limitations & Tips @@ -207,5 +221,6 @@ orderBy: { dealstage: 'asc' } - Unexpected duplicate rows? Add more group keys or remove unnecessary keys. ## See Also -- [Select Expressions](./select.md) -- SDK references: TypeScript / Python / REST *get records* pages + +- [Select Expressions](/reference/select-expressions) +- SDK references: TypeScript / Python / REST _get records_ pages diff --git a/docs/docs/concepts/search/pagination-order.md b/docs/docs/learn/search-query/pagination-order.md similarity index 97% rename from docs/docs/concepts/search/pagination-order.md rename to docs/docs/learn/search-query/pagination-order.md index d5308ebd..b1843ed0 100644 --- a/docs/docs/concepts/search/pagination-order.md +++ b/docs/docs/learn/search-query/pagination-order.md @@ -1,4 +1,5 @@ --- +slug: /reference/pagination-order sidebar_position: 3 --- @@ -231,4 +232,4 @@ In the early order case, only the first 100 deals (by default ID ordering) contr - Use early ordering intentionally only when you want to compute metrics over a _windowed_ subset (e.g., rolling sample of newest records). - The behavior applies to any grouped aggregation, not just the self-group pattern. -See also: [Select Expressions guide](./select.md#ordering-by-select-keys-late-order--pagination) for more context. +See also: [Select Expressions guide](/reference/select-expressions#ordering-by-select-keys-late-order--pagination) for more context. diff --git a/docs/docs/concepts/search/labels.md b/docs/docs/learn/search-query/search-labels.md similarity index 97% rename from docs/docs/concepts/search/labels.md rename to docs/docs/learn/search-query/search-labels.md index 48cbd0e1..77d81060 100644 --- a/docs/docs/concepts/search/labels.md +++ b/docs/docs/learn/search-query/search-labels.md @@ -1,4 +1,5 @@ --- +slug: /reference/search-labels sidebar_position: 2 --- @@ -29,7 +30,7 @@ The simplest way to use labels is to specify a single label to filter the record ```typescript { - labels: ['PERSON'] // Only search for records with the PERSON label + labels: ['PERSON'] // Only search for records with the PERSON label } ``` @@ -43,7 +44,7 @@ When no labels are provided in the query, RushDB will search across all record l { // No 'labels' property - will search across all record types where: { - name: "John" + name: 'John' } } ``` @@ -178,6 +179,7 @@ This query counts the number of employees for each company. ``` This example searches for PRODUCT records with a price between 100 and 500 that are in stock, belong to either the "Electronics" or "Computers" category, and have reviews with ratings of at least 4. It also calculates the average rating for each product, sorts the results by price in descending order, and limits the results to 20 records. +
@@ -206,4 +208,5 @@ This example searches for PRODUCT records with a price between 100 and 500 that ``` This example searches across three types of content records (ARTICLE, BLOG_POST, and NEWS) that are published and contain either "AI" in the title or "artificial intelligence" in the content, written by authors with a reputation of at least 100. It retrieves the author's name for each record, sorts the results by publication date in descending order, and limits the results to 50 records. -
\ No newline at end of file + + diff --git a/docs/docs/concepts/search/introduction.md b/docs/docs/learn/search-query/search-query.md similarity index 66% rename from docs/docs/concepts/search/introduction.md rename to docs/docs/learn/search-query/search-query.md index bde6d169..1d0f6e1c 100644 --- a/docs/docs/concepts/search/introduction.md +++ b/docs/docs/learn/search-query/search-query.md @@ -1,4 +1,5 @@ --- +slug: /reference/search-query title: Introduction sidebar_position: 0 --- @@ -11,12 +12,12 @@ RushDB provides a powerful and flexible search system that allows you to efficie RushDB's Search API offers a comprehensive set of features: -- **Powerful Filtering**: Use the [`where` clause](./where.md) with a wide range of operators to precisely filter records +- **Powerful Filtering**: Use the [`where` clause](/reference/where-operators) with a wide range of operators to precisely filter records - **Graph Traversal**: Navigate through connected records with relationship queries -- **Select Expressions**: Compute aggregates, derive metrics, and shape output using the [`select` clause](./select.md) -- **Pagination and Sorting**: Control result volume and order with [pagination and sorting options](./pagination-order.md) -- **Label-Based Filtering**: Target specific types of records using [label filtering](./labels.md) -- **Semantic Search**: Find records by meaning using AI embedding indexes — see [AI Search](../../rest-api/ai/overview) +- **Select Expressions**: Compute aggregates, derive metrics, and shape output using the [`select` clause](/reference/select-expressions) +- **Pagination and Sorting**: Control result volume and order with [pagination and sorting options](/reference/pagination-order) +- **Label-Based Filtering**: Target specific types of records using [label filtering](/reference/search-labels) +- **Semantic Search**: Find records by meaning using AI embedding indexes — see [AI Search](/rest-api/ai/overview) ## SearchQuery Structure @@ -24,12 +25,12 @@ All search operations in RushDB use a consistent SearchQuery data structure: ```typescript interface SearchQuery { - labels?: string[]; // Filter by record labels - where?: WhereCondition; // Filter by property values and relationships - limit?: number; // Maximum number of records to return (default: 100) - skip?: number; // Number of records to skip (for pagination) - orderBy?: OrderByClause; // Sorting configuration - select?: SelectExprMap; // Output-shaping expressions + labels?: string[] // Filter by record labels + where?: WhereCondition // Filter by property values and relationships + limit?: number // Maximum number of records to return (default: 100) + skip?: number // Number of records to skip (for pagination) + orderBy?: OrderByClause // Sorting configuration + select?: SelectExprMap // Output-shaping expressions } ``` @@ -39,20 +40,21 @@ Here's a simple example of searching for products: ```typescript db.records.find({ - labels: ["PRODUCT"], - where: { - title: { $contains: "Sneakers" }, - SIZE: { - uk: { $gte: 8, $lte: 9 }, - qty: { $gt: 0 } - } - }, - limit: 20, - orderBy: { price: 'asc' } + labels: ['PRODUCT'], + where: { + title: { $contains: 'Sneakers' }, + SIZE: { + uk: { $gte: 8, $lte: 9 }, + qty: { $gt: 0 } + } + }, + limit: 20, + orderBy: { price: 'asc' } }) ``` This query: + 1. Searches for records with the `PRODUCT` label 2. Filters for products with "Sneakers" in the title 3. Finds only those with UK sizes 8-9 that are in stock @@ -63,7 +65,7 @@ This query: ### Filtering with Where Clauses -The [`where` clause](./where.md) is the primary mechanism for filtering records. It supports: +The [`where` clause](/reference/where-operators) is the primary mechanism for filtering records. It supports: - **Property Matching**: Filter by exact values, string patterns, numeric ranges, etc. - **Logical Operators**: Combine conditions with AND, OR, NOT, etc. @@ -84,7 +86,7 @@ The [`where` clause](./where.md) is the primary mechanism for filtering records. ### Aggregating Results -[Select Expressions](./select.md) allow you to perform calculations and transform the structure of your results: +[Select Expressions](/reference/select-expressions) allow you to perform calculations and transform the structure of your results: ```typescript { @@ -104,7 +106,7 @@ The [`where` clause](./where.md) is the primary mechanism for filtering records. ### Pagination and Sorting -Control the volume and order of results using [pagination and sorting options](./pagination-order.md): +Control the volume and order of results using [pagination and sorting options](/reference/pagination-order): ```typescript { @@ -130,7 +132,7 @@ For optimal performance when using the Search API: ## Next Steps -- Learn more about [filtering with where clauses](./where.md) -- Explore [select expressions and aggregation capabilities](./select.md) -- Understand [pagination and sorting options](./pagination-order.md) -- Discover how to filter by [record labels](./labels.md) +- Learn more about [filtering with where clauses](/reference/where-operators) +- Explore [select expressions and aggregation capabilities](/reference/select-expressions) +- Understand [pagination and sorting options](/reference/pagination-order) +- Discover how to filter by [record labels](/reference/search-labels) diff --git a/docs/docs/concepts/search/select.md b/docs/docs/learn/search-query/select-expressions.md similarity index 73% rename from docs/docs/concepts/search/select.md rename to docs/docs/learn/search-query/select-expressions.md index 6d767743..458fccd2 100644 --- a/docs/docs/concepts/search/select.md +++ b/docs/docs/learn/search-query/select-expressions.md @@ -1,4 +1,5 @@ --- +slug: /reference/select-expressions sidebar_position: 7 --- @@ -30,24 +31,24 @@ The `select` key sits at the same level as `where`, `limit`, `orderBy`, and `lab ## Expression Reference -| Expression | Description | -|---|---| -| `"$alias.field"` | Field reference — reads `field` from the record at `$alias` | -| `"$alias"` | Alias-only reference — the record node itself | -| `number` | Literal number | -| `boolean` | Literal boolean | -| `{ $ref: "key" }` | Reference another output key in the same `select` map | -| `{ $sum: expr }` | Sum of a numeric expression | -| `{ $avg: expr, $precision?: n }` | Average; `$precision` controls decimal places (0 = integer) | -| `{ $count: '*' \| expr }` | `'*'` counts distinct root records; expression counts distinct values | -| `{ $min: expr }` | Minimum value | -| `{ $max: expr }` | Maximum value | -| `{ $divide: [expr, expr] }` | Division | -| `{ $multiply: [expr, expr] }` | Multiplication | -| `{ $add: [expr, expr] }` | Addition | -| `{ $subtract: [expr, expr] }` | Subtraction | -| `{ $collect: CollectExpr }` | Collect related records into an array | -| `{ $timeBucket: TimeBucketExpr }` | Bucket a datetime field into calendar intervals | +| Expression | Description | +| --------------------------------- | --------------------------------------------------------------------- | +| `"$alias.field"` | Field reference — reads `field` from the record at `$alias` | +| `"$alias"` | Alias-only reference — the record node itself | +| `number` | Literal number | +| `boolean` | Literal boolean | +| `{ $ref: "key" }` | Reference another output key in the same `select` map | +| `{ $sum: expr }` | Sum of a numeric expression | +| `{ $avg: expr, $precision?: n }` | Average; `$precision` controls decimal places (0 = integer) | +| `{ $count: '*' \| expr }` | `'*'` counts distinct root records; expression counts distinct values | +| `{ $min: expr }` | Minimum value | +| `{ $max: expr }` | Maximum value | +| `{ $divide: [expr, expr] }` | Division | +| `{ $multiply: [expr, expr] }` | Multiplication | +| `{ $add: [expr, expr] }` | Addition | +| `{ $subtract: [expr, expr] }` | Subtraction | +| `{ $collect: CollectExpr }` | Collect related records into an array | +| `{ $timeBucket: TimeBucketExpr }` | Bucket a datetime field into calendar intervals | ## Field References @@ -168,18 +169,18 @@ Supported: `$add`, `$subtract`, `$multiply`, `$divide` — each accepts `[expr, // Type reference type CollectExpr = { // ── alias-based (requires $alias in where) ── - from?: string // "$alias" declared in where + from?: string // "$alias" declared in where // ── label-based (inline traversal — no alias required) ── - label?: string // related record label to traverse to (e.g. 'DEPARTMENT') - where?: object // flat property filter on the traversed level (label-based only) + label?: string // related record label to traverse to (e.g. 'DEPARTMENT') + where?: object // flat property filter on the traversed level (label-based only) // ── common options ── - select?: Record // field projection; nested $collect allowed (label-based) - orderBy?: object // sort collected items - limit?: number // cap array length - skip?: number // skip N items - unique?: boolean // deduplicate (default: true) + select?: Record // field projection; nested $collect allowed (label-based) + orderBy?: object // sort collected items + limit?: number // cap array length + skip?: number // skip N items + unique?: boolean // deduplicate (default: true) } ``` @@ -299,6 +300,17 @@ Nest `$collect` inside `select` to traverse multiple hops in a single query — This returns a tree of `COMPANY → DEPARTMENT → PROJECT → EMPLOYEE` with no extra `where` boilerplate. +```mermaid +graph LR + COMPANY[COMPANY] + DEPARTMENT[DEPARTMENT] + PROJECT[PROJECT] + EMPLOYEE[EMPLOYEE] + COMPANY -->|"departments[ ]"| DEPARTMENT + DEPARTMENT -->|"projects[ ]"| PROJECT + PROJECT -->|"employees[ ]"| EMPLOYEE +``` +
Response shape example @@ -328,6 +340,7 @@ This returns a tree of `COMPANY → DEPARTMENT → PROJECT → EMPLOYEE` with no success: true } ``` +
## `$timeBucket` — Calendar Grouping @@ -337,27 +350,27 @@ This returns a tree of `COMPANY → DEPARTMENT → PROJECT → EMPLOYEE` with no ```typescript // Type reference type TimeBucketExpr = { - field: string // "$alias.fieldName" — must be a datetime field - unit: string // see unit table below - size?: number // required for plural units + field: string // "$alias.fieldName" — must be a datetime field + unit: string // see unit table below + size?: number // required for plural units } ``` -| `unit` | Description | `size?` | -|---|---|---| -| `'day'` | Calendar day | — | -| `'week'` | ISO week | — | -| `'month'` | Calendar month | — | -| `'quarter'` | 3-month quarter (starts at months 1, 4, 7, 10) | — | -| `'year'` | Calendar year | — | -| `'hour'` | Clock hour | — | -| `'minute'` | Clock minute | — | -| `'second'` | Clock second | — | -| `'months'` | N-month window | required | -| `'hours'` | N-hour window | required | -| `'minutes'` | N-minute window | required | -| `'seconds'` | N-second window | required | -| `'years'` | N-year window | required | +| `unit` | Description | `size?` | +| ----------- | ---------------------------------------------- | -------- | +| `'day'` | Calendar day | — | +| `'week'` | ISO week | — | +| `'month'` | Calendar month | — | +| `'quarter'` | 3-month quarter (starts at months 1, 4, 7, 10) | — | +| `'year'` | Calendar year | — | +| `'hour'` | Clock hour | — | +| `'minute'` | Clock minute | — | +| `'second'` | Clock second | — | +| `'months'` | N-month window | required | +| `'hours'` | N-hour window | required | +| `'minutes'` | N-minute window | required | +| `'seconds'` | N-second window | required | +| `'years'` | N-year window | required | Bucket value is the **start of the interval** as epoch milliseconds (suitable for `groupBy` comparison and sorting). @@ -424,7 +437,7 @@ If a record's field is not typed as `datetime` in RushDB metadata, its bucket va ## Grouping Results (`groupBy`) -For full grouping semantics see the [Grouping guide](./group-by.md). Quick reference: +For full grouping semantics see the [Grouping guide](/reference/group-by). Quick reference: ```typescript { @@ -451,13 +464,13 @@ For full grouping semantics see the [Grouping guide](./group-by.md). Quick refer } ``` -This produces a single-row result array with both metrics. See the [Grouping guide](./group-by.md#grouping-only-by-an-aggregated-value-self-group) for details. +This produces a single-row result array with both metrics. See the [Grouping guide](/reference/group-by#grouping-only-by-an-aggregated-value-self-group) for details. ## Ordering by `select` Keys (Late Order & Pagination) -When you reference a `select` output key in `orderBy`, RushDB defers `ORDER BY` and pagination until *after* the aggregation step. This guarantees correct totals across the full matching record set. +When you reference a `select` output key in `orderBy`, RushDB defers `ORDER BY` and pagination until _after_ the aggregation step. This guarantees correct totals across the full matching record set. -If `orderBy` does not reference a `select` key, pagination happens *before* aggregation and may produce underreported totals. +If `orderBy` does not reference a `select` key, pagination happens _before_ aggregation and may produce underreported totals. Example (ordering on an aggregated key — correct full aggregation): @@ -482,10 +495,11 @@ RETURN {`totalAmount`:`totalAmount`} as records ``` Guidelines: + - Always specify `orderBy` on a `select` key when using `groupBy` to get accurate totals across the entire match set. - Omit it only if you intentionally want to aggregate over a pre-sliced subset of records. -See also: [Pagination & Order guide](./pagination-order.md#ordering-with-aggregations). +See also: [Pagination & Order guide](/reference/pagination-order#ordering-with-aggregations). ## Complete Example @@ -547,28 +561,3 @@ See also: [Pagination & Order guide](./pagination-order.md#ordering-with-aggrega ``` - ---- - -:::info Migration reference -If you have existing queries using the legacy `aggregate` DSL, use this mapping to migrate to `select`. The `aggregate` clause is deprecated and should only be used for vector similarity until `select` supports it: - -| Legacy `aggregate` | New `select` | -|---|---| -| `'$record.name'` (inline ref) | `'$record.name'` *(unchanged)* | -| `{ fn: 'sum', field: 'salary', alias: '$emp' }` | `{ $sum: '$emp.salary' }` | -| `{ fn: 'avg', field: 'salary', alias: '$emp', precision: 2 }` | `{ $avg: '$emp.salary', $precision: 2 }` | -| `{ fn: 'count', alias: '$record' }` | `{ $count: '*' }` | -| `{ fn: 'count', field: 'x', alias: '$a' }` | `{ $count: '$a.x' }` | -| `{ fn: 'min', field: 'salary', alias: '$emp' }` | `{ $min: '$emp.salary' }` | -| `{ fn: 'max', field: 'salary', alias: '$emp' }` | `{ $max: '$emp.salary' }` | -| `{ fn: 'collect', alias: '$emp', field: 'name' }` | `{ $collect: { from: '$emp', select: { name: '$emp.name' } } }` | -| `{ fn: 'collect', alias: '$emp' }` | `{ $collect: { from: '$emp' } }` | -| Nested `fn:'collect'` in `aggregate` | `{ $collect: { label: 'LABEL', select: { ..., nested: { $collect: { label: '...' } } } } }` | -| `{ fn: 'timeBucket', field: 'date', granularity: 'day' }` | `{ $timeBucket: { field: '$record.date', unit: 'day' } }` | - -> **Note on vector similarity**: `fn: 'vector.similarity.cosine'` and `fn: 'vector.similarity.euclidean'` are not yet available in `select`. Use the legacy `aggregate` clause ONLY for these queries until the `select` equivalent is released. All other metrics/analytics must use `select`. -::: - - - diff --git a/docs/docs/concepts/search/where.md b/docs/docs/learn/search-query/where-operators.md similarity index 83% rename from docs/docs/concepts/search/where.md rename to docs/docs/learn/search-query/where-operators.md index 97c1d1c6..c377b4ad 100644 --- a/docs/docs/concepts/search/where.md +++ b/docs/docs/learn/search-query/where-operators.md @@ -1,4 +1,5 @@ --- +slug: /reference/where-operators sidebar_position: 1 --- @@ -49,7 +50,9 @@ The `$contains` operator checks if a string field contains the specified substri ```typescript { where: { - name: { $contains: "John" } // Matches "John", "Johnny", "Johnson", etc. + name: { + $contains: 'John' + } // Matches "John", "Johnny", "Johnson", etc. } } ``` @@ -61,7 +64,9 @@ The `$startsWith` operator checks if a string field starts with the specified su ```typescript { where: { - name: { $startsWith: "J" } // Matches "John", "Jane", "James", etc. + name: { + $startsWith: 'J' + } // Matches "John", "Jane", "James", etc. } } ``` @@ -73,7 +78,9 @@ The `$endsWith` operator checks if a string field ends with the specified substr ```typescript { where: { - name: { $endsWith: "son" } // Matches "Johnson", "Jackson", etc. + name: { + $endsWith: 'son' + } // Matches "Johnson", "Jackson", etc. } } ``` @@ -85,7 +92,9 @@ The `$in` operator checks if a string field matches any value in the specified a ```typescript { where: { - status: { $in: ["active", "pending"] } // Matches "active" or "pending" + status: { + $in: ['active', 'pending'] + } // Matches "active" or "pending" } } ``` @@ -97,7 +106,9 @@ The `$nin` operator checks if a string field does not match any value in the spe ```typescript { where: { - status: { $nin: ["deleted", "archived"] } // Matches anything except "deleted" or "archived" + status: { + $nin: ['deleted', 'archived'] + } // Matches anything except "deleted" or "archived" } } ``` @@ -109,7 +120,9 @@ The `$ne` operator checks if a string field is not equal to the specified value. ```typescript { where: { - status: { $ne: "deleted" } // Matches anything except "deleted" + status: { + $ne: 'deleted' + } // Matches anything except "deleted" } } ``` @@ -123,7 +136,9 @@ The `$gt` (greater than) operator checks if a number field is greater than the s ```typescript { where: { - age: { $gt: 18 } // Matches age greater than 18 + age: { + $gt: 18 + } // Matches age greater than 18 } } ``` @@ -135,7 +150,9 @@ The `$gte` (greater than or equal to) operator checks if a number field is great ```typescript { where: { - age: { $gte: 21 } // Matches age 21 or greater + age: { + $gte: 21 + } // Matches age 21 or greater } } ``` @@ -147,7 +164,9 @@ The `$lt` (less than) operator checks if a number field is less than the specifi ```typescript { where: { - age: { $lt: 65 } // Matches age less than 65 + age: { + $lt: 65 + } // Matches age less than 65 } } ``` @@ -159,7 +178,9 @@ The `$lte` (less than or equal to) operator checks if a number field is less tha ```typescript { where: { - age: { $lte: 64 } // Matches age 64 or less + age: { + $lte: 64 + } // Matches age 64 or less } } ``` @@ -171,7 +192,9 @@ The `$in` operator checks if a number field matches any value in the specified a ```typescript { where: { - age: { $in: [20, 30, 40] } // Matches age 20, 30, or 40 + age: { + $in: [20, 30, 40] + } // Matches age 20, 30, or 40 } } ``` @@ -183,7 +206,9 @@ The `$nin` operator checks if a number field does not match any value in the spe ```typescript { where: { - age: { $nin: [20, 30, 40] } // Matches any age except 20, 30, or 40 + age: { + $nin: [20, 30, 40] + } // Matches any age except 20, 30, or 40 } } ``` @@ -195,7 +220,9 @@ The `$ne` operator checks if a number field is not equal to the specified value. ```typescript { where: { - age: { $ne: 18 } // Matches any age except 18 + age: { + $ne: 18 + } // Matches any age except 18 } } ``` @@ -207,7 +234,7 @@ The `$ne` operator checks if a number field is not equal to the specified value. ```typescript { where: { - isActive: true // Matches records where isActive is true + isActive: true // Matches records where isActive is true } } ``` @@ -219,7 +246,9 @@ The `$ne` operator checks if a boolean field is not equal to the specified value ```typescript { where: { - isActive: { $ne: false } // Matches records where isActive is not false (i.e., true or not set) + isActive: { + $ne: false + } // Matches records where isActive is not false (i.e., true or not set) } } ``` @@ -235,7 +264,7 @@ You can match datetime values using ISO 8601 formatted strings: ```typescript { where: { - created: "2023-01-01T00:00:00Z" // Exact datetime match + created: '2023-01-01T00:00:00Z' // Exact datetime match } } ``` @@ -257,6 +286,7 @@ You can also match based on specific datetime components: ``` Available datetime components: + - `$year`: Match by year - `$month`: Match by month (1-12) - `$day`: Match by day of month (1-31) @@ -274,7 +304,9 @@ Datetime values support comparison operators: ```typescript { where: { - created: { $gte: "2023-01-01T00:00:00Z" } // Matches dates on or after January 1, 2023 + created: { + $gte: '2023-01-01T00:00:00Z' + } // Matches dates on or after January 1, 2023 } } ``` @@ -300,11 +332,8 @@ You can also use array operators with datetime values: { where: { created: { - $in: [ - "2023-01-01T00:00:00Z", - "2023-02-01T00:00:00Z" - ] - } // Matches either of these two specific dates + $in: ['2023-01-01T00:00:00Z', '2023-02-01T00:00:00Z'] + } // Matches either of these two specific dates } } ``` @@ -317,7 +346,7 @@ You can also use array operators with datetime values: { $year: 2020, $month: 1, $day: 1 }, { $year: 2021, $month: 1, $day: 1 } ] - } // Matches dates that are not January 1 of 2020 or 2021 + } // Matches dates that are not January 1 of 2020 or 2021 } } ``` @@ -333,7 +362,9 @@ The `$exists` operator checks whether a field exists in the record or not. This ```typescript { where: { - phoneNumber: { $exists: true } // Only records that have a phoneNumber field + phoneNumber: { + $exists: true + } // Only records that have a phoneNumber field } } ``` @@ -343,12 +374,15 @@ The `$exists` operator checks whether a field exists in the record or not. This ```typescript { where: { - phoneNumber: { $exists: false } // Only records that don't have a phoneNumber field + phoneNumber: { + $exists: false + } // Only records that don't have a phoneNumber field } } ``` The `$exists` operator works with all field types (string, number, boolean, datetime, null, arrays) and considers a field to: + - **Exist** (`$exists: true`) when the field is not null and not empty - **Not exist** (`$exists: false`) when the field is null or empty @@ -358,24 +392,25 @@ The `$exists` operator works with all field types (string, number, boolean, date // Find users who have provided their email address { where: { - email: { $exists: true } + email: { + $exists: true + } } } // Find products that don't have a description { where: { - description: { $exists: false } + description: { + $exists: false + } } } // Combine with other operators { where: { - $and: [ - { email: { $exists: true } }, - { isActive: true } - ] + $and: [{ email: { $exists: true } }, { isActive: true }] } } ``` @@ -387,12 +422,15 @@ The `$type` operator checks whether a field has a specific data type. This is us ```typescript { where: { - value: { $type: "string" } // Only records where 'value' field is a string + value: { + $type: 'string' + } // Only records where 'value' field is a string } } ``` Available types: + - `"string"`: Text values - `"number"`: Numeric values - `"boolean"`: True/false values @@ -405,29 +443,31 @@ Available types: // Find records where age is actually a number (not stored as string) { where: { - age: { $type: "number" } + age: { + $type: 'number' + } } } // Find records where the status field is a boolean { where: { - status: { $type: "boolean" } + status: { + $type: 'boolean' + } } } // Combine with other operators to find string fields that contain specific text { where: { - $and: [ - { description: { $type: "string" } }, - { description: { $contains: "important" } } - ] + $and: [{ description: { $type: 'string' } }, { description: { $contains: 'important' } }] } } ``` The `$type` operator is particularly useful when: + - Working with imported data that might have inconsistent types - Validating data integrity across your records - Building queries that need to handle fields that might contain different types of values @@ -468,10 +508,7 @@ The `$or` operator returns records that match at least one of the specified cond ```typescript { where: { - $or: [ - { name: { $startsWith: "J" } }, - { age: { $gte: 21 } } - ] + $or: [{ name: { $startsWith: 'J' } }, { age: { $gte: 21 } }] } } ``` @@ -484,7 +521,7 @@ The `$not` operator inverts the specified condition, returning records that don' { where: { $not: { - status: "deleted" + status: 'deleted' } } } @@ -497,10 +534,7 @@ The `$nor` operator returns records that don't match any of the specified condit ```typescript { where: { - $nor: [ - { status: "deleted" }, - { status: "archived" } - ] + $nor: [{ status: 'deleted' }, { status: 'archived' }] } } ``` @@ -512,10 +546,7 @@ The `$xor` operator (exclusive OR) returns records that match exactly one of the ```typescript { where: { - $xor: [ - { isPremium: true }, - { hasFreeTrialAccess: true } - ] + $xor: [{ isPremium: true }, { hasFreeTrialAccess: true }] } } ``` @@ -528,12 +559,9 @@ Logical operators can be nested to create complex conditions: { where: { $or: [ - { status: "active" }, + { status: 'active' }, { - $and: [ - { status: "pending" }, - { createdAt: { $gte: "2023-01-01T00:00:00Z" } } - ] + $and: [{ status: 'pending' }, { createdAt: { $gte: '2023-01-01T00:00:00Z' } }] } ] } @@ -582,13 +610,13 @@ You can specify the direction of the relationship using the `$relation` operator } ``` -You can also use a simplified syntax for specifying just the relationship type: +You can also use a simplified syntax for specifying just the relationship type. This produces a direction-agnostic Cypher pattern (`-[:TYPE]-`) that traverses the edge regardless of its stored direction — it does not imply `direction: "out"`: ```typescript { where: { POST: { - $relation: "AUTHORED", // Relationship type only + $relation: "AUTHORED", // Relationship type — direction-agnostic title: { $contains: "Graph" } // Property on the related record } } @@ -617,6 +645,17 @@ You can query through multiple levels of relationships: This will find records connected to a DEPARTMENT named "Engineering" that has a PROJECT named "Database" with at least one EMPLOYEE with the role "Developer". +```mermaid +graph LR + ROOT["Root Record"] + DEPT["DEPARTMENT
name: Engineering"] + PROJ["PROJECT
name: Database"] + EMP["EMPLOYEE
role: Developer"] + ROOT --> DEPT + DEPT --> PROJ + PROJ --> EMP +``` + ### Aliasing for Aggregations When you need to reference related records in aggregations, use the `$alias` operator: @@ -635,7 +674,7 @@ When you need to reference related records in aggregations, use the `$alias` ope } ``` -> For more detailed information about aggregations, including available functions and advanced usage, see [Select Expressions](./select.md). +> For more detailed information about aggregations, including available functions and advanced usage, see [Select Expressions](/reference/select-expressions). ### ID Filtering @@ -657,7 +696,9 @@ The `$id` operator supports all the string comparison operators: ```typescript { where: { - $id: { $in: ["123456", "789012"] } + $id: { + $in: ['123456', '789012'] + } } } ``` @@ -674,12 +715,12 @@ You can combine logical operators with relationships to create powerful queries. $or: [ { DEPARTMENT: { - name: "Engineering" + name: 'Engineering' } }, { DEPARTMENT: { - name: "Product" + name: 'Product' } } ] @@ -697,7 +738,7 @@ This will find records connected to either a DEPARTMENT named "Engineering" OR a $and: [ { DEPARTMENT: { - name: "Engineering" + name: 'Engineering' } }, { @@ -781,6 +822,7 @@ You can use logical operators inside relationship queries: ``` This query finds records whose name starts with "Tech", founded in or after 2010, that are active, and are in the Software, AI, or Cloud industry. +
@@ -811,6 +853,7 @@ This query finds records whose name starts with "Tech", founded in or after 2010 ``` This query traverses a complex relationship structure from COMPANY to DEPARTMENT to PROJECT to EMPLOYEE, applying filters at each level. +
@@ -842,6 +885,7 @@ This query traverses a complex relationship structure from COMPANY to DEPARTMENT ``` This query uses nested logical operators to find records that either have a high rating with moderate review count OR a slightly lower rating with high review count and are featured, but in either case are not deprecated. +
@@ -851,13 +895,13 @@ This query uses nested logical operators to find records that either have a high { where: { $and: [ - { email: { $exists: true } }, // Must have email + { email: { $exists: true } }, // Must have email { phoneNumber: { $exists: false } }, // Must not have phone number { isActive: true }, { $or: [ - { lastLoginDate: { $exists: true } }, // Has logged in before - { createdAt: { $gte: "2024-01-01T00:00:00Z" } } // Or is a recent signup + { lastLoginDate: { $exists: true } }, // Has logged in before + { createdAt: { $gte: '2024-01-01T00:00:00Z' } } // Or is a recent signup ] } ] @@ -866,6 +910,7 @@ This query uses nested logical operators to find records that either have a high ``` This query finds active users who have provided an email address but no phone number, and either have logged in before or are recent signups. +
@@ -885,6 +930,7 @@ This query finds active users who have provided an email address but no phone nu ``` This query finds records related to documents about neural networks that have chunks mentioning "embedding". For semantic (embedding-based) search, use the dedicated [AI search endpoint](/rest-api/ai/search). +
@@ -911,6 +957,7 @@ This query finds records related to documents about neural networks that have ch ``` This query finds records created in 2023 by authors with a reputation of at least 100 who have at least one non-deleted post. +
## Additional Notes diff --git a/docs/docs/learn/semantic-search/bring-your-own-vectors.mdx b/docs/docs/learn/semantic-search/bring-your-own-vectors.mdx new file mode 100644 index 00000000..7cb9cfa0 --- /dev/null +++ b/docs/docs/learn/semantic-search/bring-your-own-vectors.mdx @@ -0,0 +1,614 @@ +--- +slug: /build/ai-search/bring-your-own-vectors +sidebar_position: 4 +title: Bring Your Own Vectors +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Bring Your Own Vectors (BYOV) + +**External indexes** let you supply pre-computed embedding vectors instead of having the server compute them. Use them when you need: + +- A custom or private model the server cannot access +- Multimodal embeddings (image, audio, document structure) +- Vectors already produced by your ML pipeline +- Reproducible embeddings not tied to the server's active model + +--- + +## External vs Managed Comparison + +| | Managed | External | +| ----------------------------- | -------------------------------- | -------------------------------------------------------------- | +| `sourceType` | `"managed"` | `"external"` | +| Initial status | `"pending"` | `"awaiting_vectors"` | +| Who computes embeddings | RushDB server (configured model) | Your application | +| `dimensions` required | No (uses server default) | **Yes** | +| Backfill for existing records | Automatic | Manual via `upsertVectors` / `upsert_vectors` or inline writes | + +--- + +## Create an External Index + +An external index starts with status `awaiting_vectors` and transitions to `ready` once at least one vector has been written. + + + + +`db.ai.indexes.create()` + +```python +response = db.ai.indexes.create({ + "label": "Article", + "propertyName": "body", + "sourceType": "external", + "dimensions": 768, + "similarityFunction": "cosine", +}) +print(response.data["status"]) # 'awaiting_vectors' +``` + +> `dimensions` is **required** for external indexes — the server cannot infer it without an embedding model. + + + + +`db.ai.indexes.create()` + +```typescript +// Shorthand: external: true +const { data: extIndex } = await db.ai.indexes.create({ + label: 'Article', + propertyName: 'body', + external: true, + dimensions: 768, + similarityFunction: 'cosine' +}) +// extIndex.sourceType === 'external' +// extIndex.status === 'awaiting_vectors' + +// Explicit: sourceType: 'external' +const { data: extIndex } = await db.ai.indexes.create({ + label: 'Article', + propertyName: 'body', + sourceType: 'external', + dimensions: 768, + similarityFunction: 'cosine' +}) +``` + + + + +`POST /api/v1/ai/indexes` + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "Article", + "propertyName": "body", + "sourceType": "external", + "dimensions": 768, + "similarityFunction": "cosine" + }' +``` + +```json +{ + "data": { + "id": "idx_abc", + "label": "Article", + "propertyName": "body", + "sourceType": "external", + "status": "awaiting_vectors", + "dimensions": 768, + "similarityFunction": "cosine" + }, + "success": true +} +``` + + + + +--- + +## Bulk-Upsert Vectors + +Use `upsertVectors` / `upsert_vectors` to seed an external index from an existing dataset or batch pipeline. The request is **idempotent** — calling it again with the same `recordId` replaces the stored vector. + + + + +`db.ai.indexes.upsert_vectors(index_id, params)` + +```python +# Fetch records and embed with your own model +records_response = db.records.find({"where": {"__label": "Article"}}) + +items = [] +for record in records_response.data: + vector = my_embedder.embed(record["body"]) + items.append({"recordId": record["__id"], "vector": vector}) + +db.ai.indexes.upsert_vectors(ext_index_id, {"items": items}) +``` + + + + +`db.ai.indexes.upsertVectors(indexId, payload)` + +```typescript +const { data: records } = await db.records.find({ + where: { __label: 'Article' } +}) + +const myEmbedder = new MyEmbeddingModel() +const items = await Promise.all( + records.map(async (record) => ({ + recordId: record.__id, + vector: await myEmbedder.embed(record.body) + })) +) + +await db.ai.indexes.upsertVectors(extIndex.id, { items }) +``` + + + + +`POST /api/v1/ai/indexes/:id/vectors/upsert` + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/indexes/$INDEX_ID/vectors/upsert \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "items": [ + { "recordId": "rec_abc", "vector": [0.1, 0.2, 0.3] }, + { "recordId": "rec_def", "vector": [0.4, 0.5, 0.6] } + ] + }' +``` + + + + +--- + +## Inline Write (Preferred for New Records) + +Instead of a two-step create → upsert_vectors flow, write vectors inline with any record write operation. See [Write Records with Vectors](/build/ai-search/write-with-vectors) for the full reference. + + + + +```python +# One step: create record AND write its vector +record = db.records.create( + label="Article", + data={"title": "Warp drives", "body": "Alcubierre metric..."}, + vectors=[{"propertyName": "body", "vector": my_embedder.embed("Alcubierre metric...")}], +) +``` + + + + +```typescript +// One step: create record AND write its vector +const { data: record } = await db.records.create({ + label: 'Article', + data: { title: 'Warp drives', body: 'Alcubierre metric...' }, + vectors: [{ propertyName: 'body', vector: myVec }] +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Article", + "data": { "title": "Warp drives", "body": "Alcubierre metric..." }, + "vectors": [{ "propertyName": "body", "vector": [0.1, 0.2, 0.3] }] + }' +``` + + + + +--- + +## Disambiguation + +When the same `(label, propertyName)` pair has multiple external indexes (e.g. cosine and euclidean), specify `similarityFunction` to resolve which index to use. + + + + +```python +# Create two indexes on the same property +db.ai.indexes.create({ + "label": "Product", "propertyName": "embedding", + "sourceType": "external", "similarityFunction": "cosine", "dimensions": 768, +}) +db.ai.indexes.create({ + "label": "Product", "propertyName": "embedding", + "sourceType": "external", "similarityFunction": "euclidean", "dimensions": 768, +}) + +# ✅ Write to the cosine index only +db.records.create( + label="Product", + data={"name": "Widget"}, + vectors=[{ + "propertyName": "embedding", + "vector": vec, + "similarityFunction": "cosine", # required when ambiguous + }], +) + +# ✅ Search the euclidean index only +db.ai.search({ + "labels": ["Product"], + "propertyName": "embedding", + "queryVector": vec, + "similarityFunction": "euclidean", +}) + +# ❌ Missing similarityFunction → 422 Unprocessable Entity +db.records.create( + label="Product", + data={"name": "Gadget"}, + vectors=[{"propertyName": "embedding", "vector": vec}], # ambiguous! +) +``` + + + + +```typescript +// Create two indexes on the same property +await db.ai.indexes.create({ + label: 'Product', + propertyName: 'embedding', + external: true, + similarityFunction: 'cosine', + dimensions: 768 +}) +await db.ai.indexes.create({ + label: 'Product', + propertyName: 'embedding', + external: true, + similarityFunction: 'euclidean', + dimensions: 768 +}) + +// ✅ Write to cosine index only +await db.records.create({ + label: 'Product', + data: { name: 'Widget' }, + vectors: [ + { + propertyName: 'embedding', + vector: vec, + similarityFunction: 'cosine' // required when ambiguous + } + ] +}) + +// ✅ Search euclidean index only +await db.ai.search({ + labels: ['Product'], + propertyName: 'embedding', + queryVector: vec, + similarityFunction: 'euclidean' // required when ambiguous +}) + +// ❌ Omitting similarityFunction when two indexes exist → 422 Unprocessable Entity +await db.records.create({ + label: 'Product', + data: { name: 'Gadget' }, + vectors: [{ propertyName: 'embedding', vector: vec }] // ambiguous! +}) +``` + + + + +```bash +# ✅ Write to cosine index only +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Product", + "data": { "name": "Widget" }, + "vectors": [ + { "propertyName": "embedding", "vector": [0.1, 0.9], "similarityFunction": "cosine" } + ] + }' +``` + + + + +### Index signature uniqueness + +Two index policies are considered **identical** (and a second `create` returns `409 Conflict`) when all five fields match: + +| Field | Effect on uniqueness | +| -------------------- | -------------------- | +| `label` | ✅ | +| `propertyName` | ✅ | +| `sourceType` | ✅ | +| `similarityFunction` | ✅ | +| `dimensions` | ✅ | + +--- + +## Complete BYOV Worked Example + + + + +```python +from rushdb import RushDB + +db = RushDB("your-api-key") + +# 1. Create the external index +idx_response = db.ai.indexes.create({ + "label": "Doc", + "propertyName": "content", + "sourceType": "external", + "dimensions": 3, + "similarityFunction": "cosine", +}) +ext_index_id = idx_response.data["id"] + +# 2. Create records with inline vectors (one round trip per record) +articles = [ + {"title": "Alpha", "content": "First article", "vector": [1, 0, 0]}, + {"title": "Beta", "content": "Second article", "vector": [0, 1, 0]}, + {"title": "Gamma", "content": "Third article", "vector": [0, 0, 1]}, +] + +for article in articles: + db.records.create( + label="Doc", + data={"title": article["title"], "content": article["content"]}, + vectors=[{"propertyName": "content", "vector": article["vector"]}], + ) + +# 3. Search using a pre-computed query vector +response = db.ai.search({ + "labels": ["Doc"], + "propertyName": "content", + "queryVector": [1, 0, 0], # closest to Alpha + "limit": 3, +}) + +print(response.data[0].get("title")) # "Alpha" +print(response.data[0].get("__score")) # ~1.0 +``` + + + + +```typescript +import RushDB from '@rushdb/javascript-sdk' + +const db = new RushDB('your-api-key') + +// 1. Create the external index +const { data: idx } = await db.ai.indexes.create({ + label: 'Doc', + propertyName: 'content', + external: true, + dimensions: 3, + similarityFunction: 'cosine' +}) + +// 2. Create records + write inline vectors (one round trip per record) +const articles = [ + { title: 'Alpha', content: 'First article', vector: [1, 0, 0] }, + { title: 'Beta', content: 'Second article', vector: [0, 1, 0] }, + { title: 'Gamma', content: 'Third article', vector: [0, 0, 1] } +] + +for (const { title, content, vector } of articles) { + await db.records.create({ + label: 'Doc', + data: { title, content }, + vectors: [{ propertyName: 'content', vector }] + }) +} + +// 3. Search using a pre-computed query vector +const { data: results } = await db.ai.search({ + labels: ['Doc'], + propertyName: 'content', + queryVector: [1, 0, 0], // closest to Alpha + limit: 3 +}) + +console.log(results[0].data.title) // "Alpha" +console.log(results[0].data.__score) // ~1.0 +``` + + + + +```bash +# 1. Create the external index +curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"label":"Doc","propertyName":"content","sourceType":"external","dimensions":3,"similarityFunction":"cosine"}' + +# 2. Create a record with inline vectors +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{"label":"Doc","data":{"title":"Alpha","content":"First article"},"vectors":[{"propertyName":"content","vector":[1,0,0]}]}' + +# 3. Search using a pre-computed query vector +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"labels":["Doc"],"propertyName":"content","queryVector":[1,0,0],"limit":3}' +``` + + + + +--- + +## Batch Import with createMany + +For bulk seeding with flat rows, use `createMany` / `create_many` with the top-level indexed `vectors` parameter: + + + + +```python +db.records.create_many( + label="Doc", + data=[ + {"title": "Alpha", "content": "First article"}, + {"title": "Beta", "content": "Second article"}, + {"title": "Gamma", "content": "Third article"}, + ], + vectors=[ + [{"propertyName": "content", "vector": [1, 0, 0]}], # row 0 + [{"propertyName": "content", "vector": [0, 1, 0]}], # row 1 + [{"propertyName": "content", "vector": [0, 0, 1]}], # row 2 + ], +) +``` + + + + +```typescript +await db.records.createMany({ + label: 'Doc', + data: [ + { title: 'Alpha', content: 'First article' }, + { title: 'Beta', content: 'Second article' }, + { title: 'Gamma', content: 'Third article' } + ], + vectors: [ + [{ propertyName: 'content', vector: [1, 0, 0] }], + [{ propertyName: 'content', vector: [0, 1, 0] }], + [{ propertyName: 'content', vector: [0, 0, 1] }] + ] +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Doc", + "data": [ + {"title":"Alpha","content":"First article"}, + {"title":"Beta","content":"Second article"}, + {"title":"Gamma","content":"Third article"} + ], + "vectors": [ + [{"propertyName":"content","vector":[1,0,0]}], + [{"propertyName":"content","vector":[0,1,0]}], + [{"propertyName":"content","vector":[0,0,1]}] + ] + }' +``` + +For nested JSON payloads (`importJson`), create records first then call `upsertVectors` separately to seed the index. + + + + +--- + +## Mixing Managed and External Indexes + +You can have both a managed index and an external index on the same property simultaneously: + + + + +```python +# Managed — server embeds for full-text semantic search +db.ai.indexes.create({"label": "Product", "propertyName": "description"}) + +# External — your custom multimodal model +db.ai.indexes.create({ + "label": "Product", + "propertyName": "description", + "sourceType": "external", + "dimensions": 512, + "similarityFunction": "cosine", +}) +``` + + + + +```typescript +// Managed — server embeds for full-text semantic search +await db.ai.indexes.create({ label: 'Product', propertyName: 'description' }) + +// External — your custom multimodal model +await db.ai.indexes.create({ + label: 'Product', + propertyName: 'description', + external: true, + dimensions: 512, + similarityFunction: 'cosine' +}) +``` + + + + +```bash +# Managed index +curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"label":"Product","propertyName":"description"}' + +# External index +curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"label":"Product","propertyName":"description","sourceType":"external","dimensions":512,"similarityFunction":"cosine"}' +``` + + + + +When searching against a property with both types, specify `similarityFunction` (and optionally `sourceType`) to select the target index. + +--- + +## See also + +- [Manage Embedding Indexes](/build/ai-search/manage-indexes) — list, stats, delete, lifecycle +- [Write Records with Vectors](/build/ai-search/write-with-vectors) — inline vector writes at record creation +- [Semantic Search](/build/ai-search/semantic-search) — search managed and external indexes diff --git a/docs/docs/learn/semantic-search/manage-indexes.mdx b/docs/docs/learn/semantic-search/manage-indexes.mdx new file mode 100644 index 00000000..e9f1f502 --- /dev/null +++ b/docs/docs/learn/semantic-search/manage-indexes.mdx @@ -0,0 +1,483 @@ +--- +slug: /build/ai-search/manage-indexes +sidebar_position: 2 +title: Manage Embedding Indexes +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Manage Embedding Indexes + +An **embedding index** is a policy that tells RushDB to vectorize a specific string property for a label. Once `status` is `ready`, every record matching that label+property pair is searchable via [`db.ai.search()`](/build/ai-search/semantic-search). + +Indexes are scoped to `(label, propertyName)` — `Book:description` and `Article:description` are completely independent with separate vector stores. + +--- + +## How RushDB stores embeddings + +Most databases store vectors directly on the record — polluting the schema with fields like `description_emb_v1_1536` alongside regular metadata. Agents retrieving records get back a mix of business fields and raw float arrays, making schemas noisy and hard to reason about. + +RushDB stores each vector on a dedicated **Property node**, connected to the Record via an internal edge. Your record metadata stays clean and uniform; the vector index lives separately and is only accessed when you explicitly call `db.ai.search()`. + +```mermaid +graph LR + subgraph Classical["❌ Classical approach"] + direction TB + R2["Article record"] + R2 --- F1["title: string"] + R2 --- F2["author: string"] + R2 --- F3["publishedAt: date"] + R2 --- F4["description: string"] + R2 --- F5["description_emb_v1_1536: float[1536]"] + R2 --- F6["description_emb_v2_768: float[768]"] + + style F5 fill:#7f1d1d,color:#fca5a5,stroke:#991b1b + style F6 fill:#7f1d1d,color:#fca5a5,stroke:#991b1b + end + + subgraph RushDB["✅ RushDB approach"] + direction TB + R1["Article record"] + R1 --- M1["title: string"] + R1 --- M2["author: string"] + R1 --- M3["publishedAt: date"] + R1 --- M4["description: string"] + M4 -. "vector index" .-> V1["Property node\nvector: float[1536]"] + M4 -. "vector index" .-> V2["Property node\nvector: float[768]"] + + style V1 fill:#14532d,color:#86efac,stroke:#166534 + style V2 fill:#14532d,color:#86efac,stroke:#166534 + end +``` + +Decoupling vectors from record data means: + +- **Agents see clean schemas** — no float arrays mixed with business fields +- **Multiple indexes on one property** — cosine + euclidean, different dimensions, side by side +- **Zero record writes on index create/delete** — vector policy changes don't touch your data nodes + +--- + +## Create an Index + + + + +`db.ai.indexes.create()` + +```python +# Simplest form — uses server-configured model and dimensions +response = db.ai.indexes.create({ + "label": "Article", + "propertyName": "description" +}) +print(response.data["status"]) # 'pending' → backfill starts immediately + +# With explicit parameters +response = db.ai.indexes.create({ + "label": "Article", + "propertyName": "description", + "similarityFunction": "cosine", + "dimensions": 1536 +}) +``` + + + + +`db.ai.indexes.create()` + +```typescript +// Simplest form — uses server-configured model and dimensions +const { data: index } = await db.ai.indexes.create({ + label: 'Book', + propertyName: 'description' +}) + +console.log(index.status) // 'pending' → backfill starts immediately + +// With explicit parameters +const { data: index } = await db.ai.indexes.create({ + label: 'Article', + propertyName: 'body', + similarityFunction: 'cosine', + dimensions: 1536 +}) +``` + + + + +`POST /api/v1/ai/indexes` + +```bash +# Simplest form +curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"label": "Article", "propertyName": "description"}' + +# With explicit parameters +curl -X POST https://api.rushdb.com/api/v1/ai/indexes \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "label": "Article", + "propertyName": "description", + "similarityFunction": "cosine", + "dimensions": 1536 + }' +``` + + + + +### Create parameters + +| Parameter | Type | Required | Description | +| -------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------- | +| `label` | `string` | **yes** | Label to scope this index to (e.g. `"Article"`) | +| `propertyName` | `string` | **yes** | Property to embed (e.g. `"description"`) | +| `sourceType` | `string` | no | `"managed"` (default) or `"external"`. See [Bring Your Own Vectors](/build/ai-search/bring-your-own-vectors). | +| `similarityFunction` | `string` | no | `"cosine"` (default) or `"euclidean"` | +| `dimensions` | `number` | no | Vector dimensionality. Defaults to server `RUSHDB_EMBEDDING_DIMENSIONS`. **Required** for external indexes. | + +> Attempting to create a duplicate `(label, propertyName, sourceType, similarityFunction, dimensions)` tuple returns `409 Conflict`. + +> **Model config is server-side.** The embedding model is set via `RUSHDB_EMBEDDING_MODEL` and `RUSHDB_EMBEDDING_DIMENSIONS` env vars. + +### Index lifecycle + +| Status | Description | +| ------------------ | ------------------------------------------------------ | +| `pending` | Policy created, waiting for backfill scheduler | +| `indexing` | Backfill in progress | +| `awaiting_vectors` | External index — waiting for client to push vectors | +| `ready` | All existing records have vectors; search is available | +| `error` | Backfill failed; check server logs for the cause | + +--- + +## List Indexes + + + + +`db.ai.indexes.find()` + +```python +response = db.ai.indexes.find() +for index in response.data: + print(f"{index['label']}.{index['propertyName']} — {index['status']}") +``` + + + + +`db.ai.indexes.find()` + +```typescript +const { data: indexes } = await db.ai.indexes.find() +/* +[ + { + id: "01jb...", + label: "Book", + propertyName: "description", + sourceType: "managed", + similarityFunction: "cosine", + dimensions: 1536, + status: "ready", + modelKey: "text-embedding-3-small", + ... + } +] +*/ +``` + + + + +`GET /api/v1/ai/indexes` + +```bash +curl https://api.rushdb.com/api/v1/ai/indexes \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + +```json +{ + "data": [ + { + "id": "idx_abc123", + "label": "Article", + "propertyName": "description", + "sourceType": "managed", + "similarityFunction": "cosine", + "dimensions": 1536, + "status": "ready" + } + ], + "success": true +} +``` + + + + +--- + +## Index Stats + +Returns the fill rate for an index — useful for progress monitoring. + + + + +`db.ai.indexes.stats(index_id)` + +```python +response = db.ai.indexes.stats(index_id) +stats = response.data +print(f"{stats['indexedRecords']} / {stats['totalRecords']} records indexed") +``` + + + + +`db.ai.indexes.stats(id)` + +```typescript +const { data: stats } = await db.ai.indexes.stats(index.id) +console.log(`${stats.indexedRecords} / ${stats.totalRecords} records indexed`) +``` + +```typescript +type EmbeddingIndexStats = { + totalRecords: number + indexedRecords: number +} +``` + + + + +`GET /api/v1/ai/indexes/:id/stats` + +```bash +curl https://api.rushdb.com/api/v1/ai/indexes/$INDEX_ID/stats \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +--- + +## Delete an Index + + + + +`db.ai.indexes.delete(index_id)` + +```python +db.ai.indexes.delete(index_id) +``` + + + + +`db.ai.indexes.delete(id)` + +```typescript +await db.ai.indexes.delete(index.id) +``` + + + + +`DELETE /api/v1/ai/indexes/:id` + +```bash +curl -X DELETE https://api.rushdb.com/api/v1/ai/indexes/$INDEX_ID \ + -H "Authorization: Bearer $RUSHDB_API_KEY" +``` + + + + +The underlying Neo4j DDL vector index is only dropped when **zero embeddings remain** across the entire project — this avoids unnecessary rebuilds when multiple policies share the same `(dimensions, similarityFunction)`. + +--- + +## Index Response Shape + +```json +{ + "id": "idx_abc123", + "projectId": "proj_xyz", + "label": "Article", + "propertyName": "description", + "modelKey": "text-embedding-3-small", + "sourceType": "managed", + "similarityFunction": "cosine", + "dimensions": 1536, + "vectorPropertyName": "_emb_managed_cosine_1536", + "enabled": true, + "status": "ready", + "createdAt": "2025-01-10T12:00:00.000Z", + "updatedAt": "2025-01-10T12:05:00.000Z" +} +``` + +--- + +## Wait for Index Ready + +For managed indexes, backfill runs asynchronously. Poll until `status` is `ready`: + + + + +```python +import time + +def wait_for_index_ready(db, index_id, timeout_s=90): + deadline = time.time() + timeout_s + while time.time() < deadline: + response = db.ai.indexes.find() + idx = next((i for i in response.data if i["id"] == index_id), None) + if idx and idx["status"] == "ready": + return + if idx and idx["status"] == "error": + raise RuntimeError("Index entered error state") + time.sleep(3) + raise TimeoutError("Index did not become ready in time") + +response = db.ai.indexes.create({"label": "Book", "propertyName": "description"}) +wait_for_index_ready(db, response.data["id"]) +# now safe to call db.ai.search(...) +``` + + + + +```typescript +async function waitForIndexReady(db: RushDB, indexId: string, timeoutMs = 90_000): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const { data: indexes } = await db.ai.indexes.find() + const idx = indexes.find((i) => i.id === indexId) + if (idx?.status === 'ready') return + if (idx?.status === 'error') throw new Error('Index entered error state') + await new Promise((r) => setTimeout(r, 3_000)) + } + throw new Error('Index did not become ready in time') +} + +const { data: index } = await db.ai.indexes.create({ + label: 'Book', + propertyName: 'description' +}) +await waitForIndexReady(db, index.id) +// now safe to call db.ai.search(...) +``` + + + + +```bash +# Poll until status == "ready" +while true; do + STATUS=$(curl -s https://api.rushdb.com/api/v1/ai/indexes/$INDEX_ID \ + -H "Authorization: Bearer $RUSHDB_API_KEY" | jq -r '.data.status') + echo "Status: $STATUS" + [ "$STATUS" = "ready" ] && break + sleep 3 +done +``` + + + + +--- + +## Multiple Indexes on the Same Property + +You can have more than one index per `(label, propertyName)` pair, provided the signature differs: + + + + +```python +# Cosine index +db.ai.indexes.create({ + "label": "Product", + "propertyName": "description", + "similarityFunction": "cosine", + "dimensions": 768, +}) + +# Euclidean index on the same property +db.ai.indexes.create({ + "label": "Product", + "propertyName": "description", + "similarityFunction": "euclidean", + "dimensions": 768, +}) +``` + +When searching or writing vectors against a property with multiple indexes, specify `similarityFunction` to disambiguate. + + + + +```typescript +// Two indexes: Product:description/cosine and Product:description/euclidean +await db.ai.search({ + labels: ['Product'], + propertyName: 'description', + queryVector: vec, + similarityFunction: 'cosine' // required — otherwise 422 Unprocessable Entity +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["Product"], + "propertyName": "description", + "queryVector": [0.1, 0.2, 0.3], + "similarityFunction": "cosine" + }' +``` + + + + +--- + +## Error Reference + +| HTTP | Cause | +| ----- | ---------------------------------------------------------------------------------------------------------- | +| `404` | Property does not exist in the project graph | +| `409` | An index for this `(label, propertyName, sourceType, similarityFunction, dimensions)` tuple already exists | +| `422` | Property is not `string` type | +| `422` | Embedding model is not configured on the server | + +--- + +## See also + +- [Semantic Search](/build/ai-search/semantic-search) — search using managed or external indexes +- [Write Records with Vectors](/build/ai-search/write-with-vectors) — attach vectors at write time +- [Bring Your Own Vectors](/build/ai-search/bring-your-own-vectors) — external embedding workflow diff --git a/docs/docs/learn/semantic-search/semantic-search.mdx b/docs/docs/learn/semantic-search/semantic-search.mdx new file mode 100644 index 00000000..e75be700 --- /dev/null +++ b/docs/docs/learn/semantic-search/semantic-search.mdx @@ -0,0 +1,427 @@ +--- +slug: /build/ai-search/semantic-search +sidebar_position: 1 +title: Semantic Search +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Semantic Search + +`db.ai.search()` / `POST /api/v1/ai/search` performs semantic vector search across records that have an associated embedding index. Candidates are narrowed by label and `where` filters first, then ranked by cosine or euclidean similarity. + +The property referenced by `propertyName` must have a `ready` embedding index. See [Manage Embedding Indexes](/build/ai-search/manage-indexes). + +--- + +## Managed Search (Query Text) + +For a **managed** index, pass a natural-language `query` string. The server embeds it using the same model that built the index. + + + + +```python +response = db.ai.search({ + "propertyName": "description", + "query": "machine learning for beginners", + "labels": ["Article"], + "limit": 5, +}) + +for result in response.data: + print(f"[{result.get('__score'):.3f}] {result.get('title')}") +``` + + + + +```typescript +const { data: results } = await db.ai.search({ + labels: ['Book'], + propertyName: 'description', + query: 'space exploration and interstellar travel', + limit: 5 +}) + +results.forEach((r) => { + console.log(`[${r.data.__score!.toFixed(4)}] ${r.data.title}`) +}) +``` + + + + +`POST /api/v1/ai/search` + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "propertyName": "description", + "query": "fast delivery and easy returns", + "labels": ["Product"], + "limit": 5 + }' +``` + + + + +--- + +## External Search (Query Vector) + +For an **external** index, pass `queryVector` — a pre-computed embedding from your own model. No text is sent to any embedding model. + + + + +```python +vec = my_embedder.embed("machine learning for beginners") + +response = db.ai.search({ + "propertyName": "body", + "queryVector": vec, + "labels": ["Article"], + "limit": 10, +}) +``` + + + + +```typescript +const myEmbedder = new MyEmbeddingModel() +const vec = await myEmbedder.embed('space exploration') + +const { data: results } = await db.ai.search({ + labels: ['Article'], + propertyName: 'body', + queryVector: vec, + limit: 10 +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "propertyName": "body", + "queryVector": [0.1, 0.2, 0.3], + "labels": ["Article"], + "limit": 10 + }' +``` + + + + +- `query` is **not allowed** with external indexes — the server has no model to embed it. +- `queryVector` is accepted for managed indexes (bypasses server embedding). +- When `queryVector` is supplied, `dimensions` can be omitted — the server infers it from vector length. + +--- + +## Filter + Semantic Search + +The `where` clause acts as a **prefilter** — only records satisfying the filter are candidates for similarity ranking. All operators from [Where Operators](/reference/where-operators) are available. + + + + +```python +response = db.ai.search({ + "propertyName": "description", + "query": "wireless headphones", + "labels": ["Product"], + "where": { + "category": {"$eq": "electronics"}, + "inStock": {"$eq": True}, + "price": {"$lt": 100}, + }, + "limit": 20, +}) +``` + + + + +```typescript +const { data: results } = await db.ai.search({ + labels: ['Product'], + propertyName: 'description', + query: 'wireless headphones', + where: { + category: { $eq: 'electronics' }, + inStock: { $eq: true }, + price: { $lt: 100 } + }, + limit: 20 +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "propertyName": "description", + "query": "wireless headphones", + "labels": ["Product"], + "where": { + "category": {"$eq": "electronics"}, + "inStock": {"$eq": true}, + "price": {"$lt": 100} + }, + "limit": 20 + }' +``` + + + + +--- + +## Multi-Label Search + +Search across multiple entity types simultaneously. All listed labels must have an embedding index on the same `propertyName`. + + + + +```python +response = db.ai.search({ + "propertyName": "body", + "query": "machine learning trends", + "labels": ["Article", "Post", "Comment"], + "limit": 10, +}) + +for result in response.data: + print(result.get("__label"), f"{result.get('__score'):.3f}", result.get("title") or result.get("text")) +``` + + + + +```typescript +const { data: results } = await db.ai.search({ + labels: ['Article', 'Post', 'Comment'], + propertyName: 'body', + query: 'machine learning trends', + limit: 10 +}) + +results.forEach((r) => console.log(r.data.__label, r.data.__score, r.data.title ?? r.data.text)) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "propertyName": "body", + "query": "machine learning trends", + "labels": ["Article", "Post", "Comment"], + "limit": 10 + }' +``` + + + + +--- + +## Result Shape + +Results are ordered by `__score` descending — closest match first. + + + + +```python +for result in response.data: + result.id # RushDB record ID (str) + result.get("__label") # Record label (str) + result.get("__score") # Similarity score, 0–1 (float) + result.get("title") # Your fields via .get() + result["description"] # Or via [] — raises KeyError if missing +``` + + + + +```typescript +// Each result is a DBRecordInstance +// r.data.__score — similarity score (0–1, higher = more similar) +// r.data.__label — the record label +// r.data[yourField] — your properties +``` + + + + +```json +{ + "data": [ + { + "__id": "rec_abc123", + "__label": "Product", + "__score": 0.921, + "description": "Same-day shipping with hassle-free returns policy" + } + ], + "success": true +} +``` + + + + +--- + +## Parameters Reference + +| Parameter | Type | Required | Description | +| -------------------- | ------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | +| `propertyName` | `string` | **yes** | The indexed property to search against (e.g. `"description"`) | +| `labels` | `string \| string[]` | **yes** | Label(s) to search within (min 1) | +| `query` | `string` | conditionally | Free-text query. Required for managed indexes; **not allowed** for external indexes. | +| `queryVector` | `number[]` | conditionally | Pre-computed query vector. Required for external indexes. Also accepted for managed indexes. | +| `similarityFunction` | `"cosine" \| "euclidean"` | no | Required when multiple indexes target the same `(label, propertyName)`. | +| `dimensions` | `number` | no | Disambiguates when multiple indexes match. Inferred from `queryVector.length` when `queryVector` given. | +| `where` | `object` | no | Prefilter — applied before similarity scoring. | +| `skip` | `number` | no | Pagination offset (default `0`) | +| `limit` | `number` | no | Maximum results to return (default `20`) | + +--- + +## Disambiguation + +When two indexes exist for the same `(label, propertyName)`, specify `similarityFunction` to select the target index: + + + + +```python +response = db.ai.search({ + "labels": ["Product"], + "propertyName": "embedding", + "queryVector": vec, + "similarityFunction": "cosine", # required — otherwise 422 +}) +``` + + + + +```typescript +await db.ai.search({ + labels: ['Product'], + propertyName: 'embedding', + queryVector: vec, + similarityFunction: 'cosine' // required — otherwise 422 Unprocessable Entity +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "labels": ["Product"], + "propertyName": "embedding", + "queryVector": [0.1, 0.2, 0.3], + "similarityFunction": "cosine" + }' +``` + + + + +--- + +## Pagination + + + + +```python +PAGE = 20 + +page1 = db.ai.search({ + "propertyName": "description", + "query": "sustainable packaging", + "labels": ["Product"], + "limit": PAGE, + "skip": 0, +}) + +page2 = db.ai.search({ + "propertyName": "description", + "query": "sustainable packaging", + "labels": ["Product"], + "limit": PAGE, + "skip": PAGE, +}) +``` + + + + +```typescript +const PAGE = 20; + +const { data: page1 } = await db.ai.search({ ..., limit: PAGE, skip: 0 }); +const { data: page2 } = await db.ai.search({ ..., limit: PAGE, skip: PAGE }); +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/ai/search \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"propertyName": "description", "query": "...", "labels": ["Product"], "limit": 20, "skip": 20}' +``` + + + + +--- + +## Error Reference + +| HTTP | Cause | +| -------------------------- | ----------------------------------------------------------------- | +| `404 Not Found` | No enabled embedding index found for `(label, propertyName)` | +| `422 Unprocessable Entity` | Multiple indexes match and `similarityFunction` was not specified | +| `422 Unprocessable Entity` | `query` text supplied for an external index | +| `422 Unprocessable Entity` | Vector length does not match index `dimensions` | +| `503 Service Unavailable` | Embedding model unavailable (managed indexes only) | + +--- + +## See also + +- [Manage Embedding Indexes](/build/ai-search/manage-indexes) — create, list, delete, and monitor indexes +- [Write Records with Vectors](/build/ai-search/write-with-vectors) — attach vectors when storing records +- [Bring Your Own Vectors](/build/ai-search/bring-your-own-vectors) — use external embedding models +- [Find & Query](/build/data/find-and-query) — keyword / graph-only search without AI diff --git a/docs/docs/learn/semantic-search/write-with-vectors.mdx b/docs/docs/learn/semantic-search/write-with-vectors.mdx new file mode 100644 index 00000000..2796e93d --- /dev/null +++ b/docs/docs/learn/semantic-search/write-with-vectors.mdx @@ -0,0 +1,505 @@ +--- +slug: /build/ai-search/write-with-vectors +sidebar_position: 3 +title: Write Records with Vectors +--- + +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' + +# Write Records with Vectors + +RushDB lets you attach pre-computed embedding vectors to records **at write time**, eliminating the need for a separate bulk-upsert call. Any operation that creates or modifies records accepts a `vectors` parameter. + +This feature requires at least one [external index](/build/ai-search/bring-your-own-vectors) on the target `(label, propertyName)`. + +--- + +## `vectors` parameter format + +| Field | Type | Required | Description | +| -------------------- | ---------- | -------- | --------------------------------------------------------- | +| `propertyName` | `string` | **yes** | Property name this vector is associated with | +| `vector` | `number[]` | **yes** | Pre-computed embedding vector | +| `similarityFunction` | `string` | no | Required when multiple indexes exist on the same property | + +--- + +## Create a Record with Vectors + +Record creation and vector storage happen atomically in one request. + + + + +`db.records.create()` + +```python +record = db.records.create( + label="Article", + data={ + "title": "How transformers work", + "body": "Attention is all you need ...", + }, + vectors=[ + {"propertyName": "body", "vector": my_embedder.embed("Attention is all you need ...")} + ], +) +print(record.id) # record created AND vector written atomically +``` + + + + +`db.records.create()` + +```typescript +const { data: record } = await db.records.create({ + label: 'Article', + data: { + title: 'How transformers work', + body: 'Attention is all you need ...' + }, + vectors: [{ propertyName: 'body', vector: myEmbed('Attention is all you need ...') }] +}) + +console.log(record.__id) // record created AND vector written atomically +``` + + + + +`POST /api/v1/records` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Article", + "data": { + "title": "How transformers work", + "body": "Attention is all you need ..." + }, + "vectors": [ + { "propertyName": "body", "vector": [0.1, 0.2, 0.3] } + ] + }' +``` + + + + +--- + +## Upsert with Vectors + +`upsert` is idempotent on the record's natural key. Passing `vectors` writes or replaces the stored vector in the same call. + + + + +`db.records.upsert()` + +```python +# First call — creates the record + writes vector +r1 = db.records.upsert( + label="Article", + data={"slug": "transformers-101", "title": "Transformers 101", "body": "..."}, + options={"mergeBy": ["slug"], "mergeStrategy": "append"}, + vectors=[{"propertyName": "body", "vector": v1}], +) + +# Second call — same slug → updates data + replaces the vector +r2 = db.records.upsert( + label="Article", + data={"slug": "transformers-101", "title": "Transformers 101 (revised)", "body": "Updated ..."}, + options={"mergeBy": ["slug"], "mergeStrategy": "append"}, + vectors=[{"propertyName": "body", "vector": v2}], +) +# r1.__id == r2.__id +``` + + + + +`db.records.upsert()` + +```typescript +// First call — creates the record + writes vector +const { data: r1 } = await db.records.upsert({ + label: 'Article', + data: { slug: 'transformers-101', title: 'Transformers 101', body: '...' }, + vectors: [{ propertyName: 'body', vector: v1 }] +}) + +// Second call — same slug → updates data + replaces the vector +const { data: r2 } = await db.records.upsert({ + label: 'Article', + data: { slug: 'transformers-101', title: 'Transformers 101 (revised)', body: 'Updated ...' }, + vectors: [{ propertyName: 'body', vector: v2 }] +}) + +console.log(r1.__id === r2.__id) // true — same record +``` + + + + +`POST /api/v1/records` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Article", + "data": { "slug": "transformers-101", "title": "Transformers 101", "body": "..." }, + "options": { "mergeBy": ["slug"], "mergeStrategy": "append" }, + "vectors": [{ "propertyName": "body", "vector": [0.1, 0.2, 0.3] }] + }' +``` + + + + +--- + +## Set with Vectors + +`set` replaces all properties of a record. Including `vectors` writes those vectors at the same time. + + + + +`db.records.set()` + +```python +db.records.set( + target=record, + label="Product", + data={"name": "Widget Pro", "price": 19.99}, + vectors=[{"propertyName": "description", "vector": new_vec}], +) +``` + + + + +`db.records.set()` + +```typescript +await db.records.set(rec.__id, { + data: { name: 'Widget Pro', price: 19.99 }, + vectors: [{ propertyName: 'description', vector: newVec }] +}) +``` + + + + +`PUT /api/v1/records/:id` + +```bash +curl -X PUT https://api.rushdb.com/api/v1/records/rec_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Product", + "data": { "name": "Widget Pro", "price": 19.99 }, + "vectors": [{ "propertyName": "description", "vector": [0.5, 0.6, 0.7] }] + }' +``` + + + + +--- + +## Create Multiple Records with Vectors + +`createMany` / `create_many` accepts a top-level `vectors` array **indexed by row position** — each entry is a list of vector entries for that row. + + + + +`db.records.create_many()` + +```python +db.records.create_many( + label="Product", + data=[ + {"name": "Alpha", "description": "First product"}, + {"name": "Beta", "description": "Second product"}, + {"name": "Gamma", "description": "Third product"}, + ], + vectors=[ + [{"propertyName": "description", "vector": [1, 0, 0]}], # row 0 + [{"propertyName": "description", "vector": [0, 1, 0]}], # row 1 + [{"propertyName": "description", "vector": [0, 0, 1]}], # row 2 + ], +) +``` + +**Sparse vectors** — any rows beyond `vectors` length are skipped: + +```python +db.records.create_many( + label="Product", + data=[{"name": "Alpha"}, {"name": "Beta"}, {"name": "Gamma"}], + vectors=[[{"propertyName": "description", "vector": my_vec}]], # only row 0 +) +``` + + + + +`db.records.createMany()` + +```typescript +await db.records.createMany({ + label: 'Product', + data: [ + { name: 'Alpha', description: 'First product' }, + { name: 'Beta', description: 'Second product' }, + { name: 'Gamma', description: 'Third product' } + ], + vectors: [ + [{ propertyName: 'description', vector: [1, 0, 0] }], // row 0 + [{ propertyName: 'description', vector: [0, 1, 0] }], // row 1 + [{ propertyName: 'description', vector: [0, 0, 1] }] // row 2 + ], + options: { returnResult: true } +}) +``` + +**Sparse vectors** — any rows beyond `vectors.length` are skipped: + +```typescript +await db.records.createMany({ + label: 'Product', + data: [{ name: 'Alpha' }, { name: 'Beta' }, { name: 'Gamma' }], + // only row 0 gets a vector; rows 1 and 2 are skipped + vectors: [[{ propertyName: 'description', vector: myVec }]] +}) +``` + +The SDK throws synchronously if `vectors.length > data.length`: + +``` +Error: vectors length (3) exceeds the number of data rows (2) +``` + + + + +`POST /api/v1/records/import/json` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/json \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Product", + "data": [ + { "name": "Alpha", "description": "First product" }, + { "name": "Beta", "description": "Second product" }, + { "name": "Gamma", "description": "Third product" } + ], + "vectors": [ + [{ "propertyName": "description", "vector": [1, 0, 0] }], + [{ "propertyName": "description", "vector": [0, 1, 0] }], + [{ "propertyName": "description", "vector": [0, 0, 1] }] + ] + }' +``` + + + + +--- + +## Import CSV with Vectors + +CSV is a raw string, so per-row vectors are supplied separately. Row indices are 0-based after the header is consumed. + + + + +`db.records.import_csv()` + +```python +csv_data = """name,description +Alpha,First product +Beta,Second product +Gamma,Third product""" + +db.records.import_csv( + label="Product", + data=csv_data, + vectors=[ + [{"propertyName": "description", "vector": [1, 0, 0]}], # csv row 0 + [{"propertyName": "description", "vector": [0, 1, 0]}], # csv row 1 + [{"propertyName": "description", "vector": [0, 0, 1]}], # csv row 2 + ], +) +``` + + + + +`db.records.importCsv()` + +```typescript +const csv = `name,description +Alpha,First product +Beta,Second product +Gamma,Third product` + +await db.records.importCsv({ + label: 'Product', + data: csv, + vectors: [ + [{ propertyName: 'description', vector: [1, 0, 0] }], // csv row 0 + [{ propertyName: 'description', vector: [0, 1, 0] }], // csv row 1 + [{ propertyName: 'description', vector: [0, 0, 1] }] // csv row 2 + ], + options: { returnResult: true } +}) +``` + + + + +`POST /api/v1/records/import/csv` + +```bash +curl -X POST https://api.rushdb.com/api/v1/records/import/csv \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Product", + "data": "name,description\nAlpha,First product\nBeta,Second product\nGamma,Third product", + "vectors": [ + [{ "propertyName": "description", "vector": [1, 0, 0] }], + [{ "propertyName": "description", "vector": [0, 1, 0] }], + [{ "propertyName": "description", "vector": [0, 0, 1] }] + ] + }' +``` + +The server returns `400 Bad Request` if `vectors.length` exceeds the number of CSV data rows. + + + + +--- + +## Multiple vectors in one call + +Write vectors for multiple properties or indexes in a single operation: + + + + +```python +db.records.create( + label="Document", + data={"title": "Multi-modal doc", "abstract": "...", "fullText": "..."}, + vectors=[ + {"propertyName": "abstract", "vector": abstract_vec}, + {"propertyName": "fullText", "vector": full_text_vec}, + ], +) +``` + + + + +```typescript +await db.records.create({ + label: 'Document', + data: { title: 'Multi-modal doc', abstract: '...', fullText: '...' }, + vectors: [ + { propertyName: 'abstract', vector: abstractVec }, + { propertyName: 'fullText', vector: fullTextVec } + ] +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Document", + "data": { "title": "Multi-modal doc", "abstract": "...", "fullText": "..." }, + "vectors": [ + { "propertyName": "abstract", "vector": [0.1, 0.2, 0.3] }, + { "propertyName": "fullText", "vector": [0.4, 0.5, 0.6] } + ] + }' +``` + + + + +--- + +## Disambiguation + +When a `(label, propertyName)` has multiple external indexes (e.g. cosine and euclidean), specify `similarityFunction` in each vector entry to route to the correct index. Omitting it when multiple indexes match returns `422 Unprocessable Entity`. + + + + +```python +db.records.create( + label="Product", + data={"name": "Widget"}, + vectors=[ + {"propertyName": "embedding", "vector": vec, "similarityFunction": "cosine"} + ], +) +``` + + + + +```typescript +await db.records.create({ + label: 'Product', + data: { name: 'Widget' }, + vectors: [{ propertyName: 'embedding', vector: vec, similarityFunction: 'cosine' }] +}) +``` + + + + +```bash +curl -X POST https://api.rushdb.com/api/v1/records \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RUSHDB_API_KEY" \ + -d '{ + "label": "Product", + "data": { "name": "Widget" }, + "vectors": [ + { "propertyName": "embedding", "vector": [0.1, 0.9], "similarityFunction": "cosine" } + ] + }' +``` + + + + +--- + +## See also + +- [Bring Your Own Vectors](/build/ai-search/bring-your-own-vectors) — create external indexes and bulk-upsert +- [Manage Embedding Indexes](/build/ai-search/manage-indexes) — list, stats, delete indexes +- [Semantic Search](/build/ai-search/semantic-search) — search across indexed properties diff --git a/docs/docs/tutorials/agent-safe-query-planning.mdx b/docs/docs/learn/tutorials/agent-memory/agent-safe-query-planning.mdx similarity index 92% rename from docs/docs/tutorials/agent-safe-query-planning.mdx rename to docs/docs/learn/tutorials/agent-memory/agent-safe-query-planning.mdx index a4c5a8ef..66bcf703 100644 --- a/docs/docs/tutorials/agent-safe-query-planning.mdx +++ b/docs/docs/learn/tutorials/agent-memory/agent-safe-query-planning.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/agent-safe-query-planning sidebar_position: 22 -title: "Agent-Safe Query Planning with Ontology First" +title: 'Agent-Safe Query Planning with Ontology First' description: A repeatable agent pattern — ontology first, query spec second, constrained execution, and failure recovery when labels or fields are wrong. tags: [MCP, Agents, SearchQuery, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Agent-Safe Query Planning with Ontology First @@ -124,7 +125,7 @@ async function safeFind(query: Parameters[0]) { // Validate labels array const requestedLabels = query.labels ?? [] - const unknownLabels = requestedLabels.filter(l => !knownLabels.has(l)) + const unknownLabels = requestedLabels.filter((l) => !knownLabels.has(l)) if (unknownLabels.length > 0) { throw new Error( `Unknown labels: ${unknownLabels.join(', ')}. Known labels: ${[...knownLabels].join(', ')}` @@ -137,7 +138,7 @@ async function safeFind(query: Parameters[0]) { // Usage try { const result = await safeFind({ - labels: ['CUSTOMER'], // validated against ontology + labels: ['CUSTOMER'], // validated against ontology where: { status: 'active' }, limit: 10 }) @@ -223,9 +224,7 @@ async function queryWithFallback(label: string, filter: Record) // Binary-search the filter to find which clause eliminates results for (const key of Object.keys(filter)) { - const narrower = Object.fromEntries( - Object.entries(filter).filter(([k]) => k !== key) - ) + const narrower = Object.fromEntries(Object.entries(filter).filter(([k]) => k !== key)) const partial = await db.records.find({ labels: [label], where: narrower, @@ -328,10 +327,10 @@ Use the getQueryBuilderPrompt tool to load your operating instructions before ma // Inject it as system context alongside the ontology markdown const systemPrompt = [ - queryBuilderPrompt, // from getQueryBuilderPrompt MCP tool + queryBuilderPrompt, // from getQueryBuilderPrompt MCP tool '', '## Current Schema', - schemaContext // from getOntologyMarkdown + schemaContext // from getOntologyMarkdown ].join('\n') ``` @@ -381,6 +380,6 @@ Ontology grounding is a first-call overhead: one `getOntologyMarkdown` request p ## Next steps -- [Discovery Queries](./discovery-queries.mdx) — interactive schema exploration in code -- [MCP Quickstart for Real Operators](./mcp-operator-quickstart.mdx) — the same loop via MCP tools -- [Building Team Memory](./building-team-memory.mdx) — a real knowledge base for agents to query +- [Discovery Queries](/tutorials/discovery-queries) — interactive schema exploration in code +- [MCP Quickstart for Real Operators](/deploy/mcp-operator-quickstart) — the same loop via MCP tools +- [Building Team Memory](/tutorials/building-team-memory) — a real knowledge base for agents to query diff --git a/docs/docs/tutorials/agent-skills-with-openclaw.mdx b/docs/docs/learn/tutorials/agent-memory/agent-skills-with-openclaw.mdx similarity index 79% rename from docs/docs/tutorials/agent-skills-with-openclaw.mdx rename to docs/docs/learn/tutorials/agent-memory/agent-skills-with-openclaw.mdx index d250beec..17b9a273 100644 --- a/docs/docs/tutorials/agent-skills-with-openclaw.mdx +++ b/docs/docs/learn/tutorials/agent-memory/agent-skills-with-openclaw.mdx @@ -1,6 +1,7 @@ --- +slug: /tutorials/agent-skills-with-openclaw sidebar_position: 14 -title: "Using RushDB Agent Skills in OpenClaw" +title: 'Using RushDB Agent Skills in OpenClaw' description: End-to-end guide — connect RushDB to OpenClaw, install the RushDB skills pack, and let your AI assistant query, store, and model structured data without writing a single line of code. tags: [Agent Skills, OpenClaw, MCP, Agents, AI, Memory] --- @@ -31,11 +32,11 @@ Together they produce grounded, reliable RushDB sessions. ## Prerequisites -| Requirement | Notes | -|---|---| -| [OpenClaw](https://openclaw.ai/) installed and running | `openclaw gateway status` should show the gateway listening | -| RushDB account and API key | [Get API Key](../get-started/get-api-key) — free tier is enough | -| Node.js 22.14+ | OpenClaw and `npx` both need it; check with `node --version` | +| Requirement | Notes | +| ------------------------------------------------------ | ------------------------------------------------------------ | +| [OpenClaw](https://openclaw.ai/) installed and running | `openclaw gateway status` should show the gateway listening | +| RushDB account and API key | [Get API Key](/deploy/get-api-key) — free tier is enough | +| Node.js 22.14+ | OpenClaw and `npx` both need it; check with `node --version` | If you do not have OpenClaw yet, run: @@ -92,7 +93,7 @@ If the tool runs and returns results (even an empty schema), the MCP server is c The skills pack contains three skills. Install them with the native `openclaw` command or the `clawhub` CLI: -import Tabs from '@theme/TabItem'; +import Tabs from '@theme/TabItem'
Option A — OpenClaw native command (workspace-level install) @@ -102,6 +103,8 @@ import Tabs from '@theme/TabItem'; openclaw skills install rushdb/rushdb-query-builder openclaw skills install rushdb/rushdb-agent-memory openclaw skills install rushdb/rushdb-data-modeling +openclaw skills install rushdb/rushdb-faceted-search +openclaw skills install rushdb/rushdb-domain-template ``` Installed skills live in `/skills/` and are available to all agents in that workspace. @@ -115,6 +118,8 @@ Installed skills live in `/skills/` and are available to all agents i npx clawhub install rushdb/rushdb-query-builder npx clawhub install rushdb/rushdb-agent-memory npx clawhub install rushdb/rushdb-data-modeling +npx clawhub install rushdb/rushdb-faceted-search +npx clawhub install rushdb/rushdb-domain-template ``` Or browse [clawhub.ai](https://clawhub.ai/) and use the web UI to install from there. @@ -146,6 +151,10 @@ Alternatively, drop the skills into your workspace's `skills/` directory: memory-patterns.md rushdb-data-modeling/ SKILL.md + rushdb-faceted-search/ + SKILL.md + rushdb-domain-template/ + SKILL.md ```
@@ -160,7 +169,7 @@ In a new session, send: > "What skills do you have available? List them." -You should see `rushdb-query-builder`, `rushdb-agent-memory`, and `rushdb-data-modeling` in the response. +You should see `rushdb-query-builder`, `rushdb-agent-memory`, `rushdb-data-modeling`, `rushdb-faceted-search`, and `rushdb-domain-template` in the response. --- @@ -176,6 +185,8 @@ OpenClaw builds a compact catalog of all eligible skills and injects it into the /rushdb-query-builder /rushdb-agent-memory /rushdb-data-modeling +/rushdb-faceted-search +/rushdb-domain-template ``` Slash commands are useful when you want to force-load a skill at the start of a session or when the model is not auto-detecting the intent. @@ -186,7 +197,7 @@ Slash commands are useful when you want to force-load a skill at the start of a ### Skill 1: `rushdb-query-builder` -This skill enforces [the four-step discovery-first workflow](../mcp-server/introduction.mdx#mandatory-workflow). It prevents hallucinated label names and teaches the correct operator syntax. +This skill enforces [the four-step discovery-first workflow](/connect/mcp#mandatory-workflow). It prevents hallucinated label names and teaches the correct operator syntax. **Example session:** @@ -198,10 +209,7 @@ OpenClaw will: 2. Call `getSearchQuerySpec` → load the operator reference. 3. Build and execute `findRecords` with the correct `where`, `select`, and date filter. -> OpenClaw: [calls getOntologyMarkdown → confirms `ORDER`, `CUSTOMER` labels and `tier`, `createdAt` fields] -> [calls getSearchQuerySpec → loads date operator reference] -> `[calls findRecords: labels=["ORDER"], select={count:{"$count":"*"}}, where={CUSTOMER:{tier:"enterprise"},createdAt:{$gte:"2025-01-01"}}]` -> **Result:** 342 enterprise orders placed in the last 30 days. +> OpenClaw: [calls getOntologyMarkdown → confirms `ORDER`, `CUSTOMER` labels and `tier`, `createdAt` fields] > [calls getSearchQuerySpec → loads date operator reference] > `[calls findRecords: labels=["ORDER"], select={count:{"$count":"*"}}, where={CUSTOMER:{tier:"enterprise"},createdAt:{$gte:"2025-01-01"}}]` > **Result:** 342 enterprise orders placed in the last 30 days. **Slash command tip:** @@ -233,7 +241,7 @@ This skill uses RushDB as a persistent, structured memory layer. Use it when you ### Skill 3: `rushdb-data-modeling` -This skill teaches RushDB's [LMPG model](../concepts/records.md) — Labels, Metadata, Properties, and Graph relationships. Use it when designing a new data schema or evolving an existing one. +This skill teaches RushDB's [LMPG model](/build/data/store-records) — Labels, Metadata, Properties, and Graph relationships. Use it when designing a new data schema or evolving an existing one. **Example session:** @@ -246,16 +254,19 @@ This skill teaches RushDB's [LMPG model](../concepts/records.md) — Labels, Met > **Labels (UPPER_CASE):** `CUSTOMER`, `SUBSCRIPTION`, `INVOICE`, `PRICING_PLAN` > > **Relationships:** +> > - `CUSTOMER --SUBSCRIBED_TO--> SUBSCRIPTION` > - `SUBSCRIPTION --BILLED_WITH--> PRICING_PLAN` > - `SUBSCRIPTION --HAS_INVOICE--> INVOICE` > > **Key properties:** +> > - `CUSTOMER`: `email` (string), `tier` (string: "starter"/"growth"/"enterprise"), `createdAt` (datetime) > - `SUBSCRIPTION`: `status` (string: "active"/"cancelled"/"paused"), `intervalDays` (number), `startDate` (datetime) > - `INVOICE`: `amount` (number), `currency` (string), `dueDate` (datetime), `paid` (boolean) > > To import the initial data: +> > ```json > [ > { @@ -272,16 +283,17 @@ This skill teaches RushDB's [LMPG model](../concepts/records.md) — Labels, Met > } > ] > ``` +> > RushDB will auto-link the nested objects on import. --- ## What to do next -- [MCP Quickstart](../mcp-server/quickstart.mdx) — full client setup reference for Claude Desktop, Cursor, and VS Code -- [MCP Operator Quickstart](./mcp-operator-quickstart.mdx) — deep-dive on the mandatory workflow for grounded, hallucination-resistant queries -- [Building Team Memory](./building-team-memory.mdx) — using records as a shared memory layer across agents -- [Episodic Memory](./episodic-memory.mdx) — designing session and long-term memory schemas -- [Agent Memory Model](../concepts/agent-memory-model.md) — RushDB's formal model for AI memory +- [MCP Quickstart](/connect/mcp) — full client setup reference for Claude Desktop, Cursor, and VS Code +- [MCP Operator Quickstart](/deploy/mcp-operator-quickstart) — deep-dive on the mandatory workflow for grounded, hallucination-resistant queries +- [Building Team Memory](/tutorials/building-team-memory) — using records as a shared memory layer across agents +- [Episodic Memory](/tutorials/episodic-memory) — designing session and long-term memory schemas +- [Agent Memory](/build/agent-memory/) — RushDB's structured memory model for AI agents - [ClawHub](https://clawhub.ai/) — browse community skills for OpenClaw - [packages/skills README](https://github.com/rush-db/rushdb/blob/main/packages/skills/README.md) — all three RushDB skills in one place diff --git a/docs/docs/tutorials/building-team-memory.mdx b/docs/docs/learn/tutorials/agent-memory/building-team-memory.mdx similarity index 91% rename from docs/docs/tutorials/building-team-memory.mdx rename to docs/docs/learn/tutorials/agent-memory/building-team-memory.mdx index 41fddc52..12ea188f 100644 --- a/docs/docs/tutorials/building-team-memory.mdx +++ b/docs/docs/learn/tutorials/agent-memory/building-team-memory.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/building-team-memory sidebar_position: 21 -title: "Building Team Memory for Product and Support Workflows" +title: 'Building Team Memory for Product and Support Workflows' description: Ingest tickets, docs, decisions, incidents, and feature requests into a connected graph so your team can retrieve context instead of isolated documents. tags: [Memory, Agents, AI Search, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Building Team Memory for Product and Support Workflows @@ -31,14 +32,14 @@ graph LR The key labels are: -| Label | What it represents | -|---|---| -| `TICKET` | A bug report, support ticket, or task | -| `DECISION` | An architectural or product decision, ADR-style | -| `INCIDENT` | A production incident or outage | -| `DOC` | A piece of documentation, runbook, post-mortem, or RFC | +| Label | What it represents | +| ----------------- | -------------------------------------------------------- | +| `TICKET` | A bug report, support ticket, or task | +| `DECISION` | An architectural or product decision, ADR-style | +| `INCIDENT` | A production incident or outage | +| `DOC` | A piece of documentation, runbook, post-mortem, or RFC | | `FEATURE_REQUEST` | A request from customers, research, or internal feedback | -| `ALERT` | A monitoring alert that triggered during an incident | +| `ALERT` | A monitoring alert that triggered during an incident | --- @@ -188,11 +189,11 @@ After ingestion, fetch the records by their external IDs and attach causal and r // Fetch records by externalId const [ticketResult, docResult] = await Promise.all([ db.records.find({ labels: ['TICKET'], where: { externalId: 'TICKET-1002' } }), - db.records.find({ labels: ['DOC'], where: { externalId: 'DOC-201' } }) + db.records.find({ labels: ['DOC'], where: { externalId: 'DOC-201' } }) ]) const ticket = ticketResult.data[0] -const doc = docResult.data[0] +const doc = docResult.data[0] // TICKET --RESOLVED_BY--> DOC (postmortem) await db.records.attach({ @@ -204,7 +205,7 @@ await db.records.attach({ // Also link the SSO ticket to the SSO ADR doc const [ssoTicket, ssoDoc] = await Promise.all([ db.records.find({ labels: ['TICKET'], where: { externalId: 'TICKET-1001' } }), - db.records.find({ labels: ['DOC'], where: { externalId: 'DOC-007' } }) + db.records.find({ labels: ['DOC'], where: { externalId: 'DOC-007' } }) ]) await db.records.attach({ @@ -544,7 +545,7 @@ curl -s -X POST "$BASE/ai/search" \ Once the base graph is established, enrich it incrementally: -- **Webhooks from Linear/GitHub/PagerDuty** — auto-create TICKET and INCIDENT records on event (see [Event-Driven Ingestion](./event-driven-ingestion.mdx)) +- **Webhooks from Linear/GitHub/PagerDuty** — auto-create TICKET and INCIDENT records on event (see [Event-Driven Ingestion](/tutorials/event-driven-ingestion)) - **Decisions as first-class nodes** — when a team makes a decision, create a DECISION record and link it to the TICKET or INCIDENT it resolved - **FEATURE_REQUEST linked to TICKET** — when a customer request drives a bug fix or feature, link the FEATURE_REQUEST to the TICKET so the team can see customer impact upstream of every change @@ -558,6 +559,6 @@ Team memory graphs grow unboundedly unless pruned. Define a retention policy for ## Next steps -- [Agent-Safe Query Planning with Ontology First](./agent-safe-query-planning.mdx) — run agents over this memory graph safely -- [Episodic Memory for Multi-Step Agents](./episodic-memory.mdx) — session-scoped context alongside persistent team memory -- [Data Lineage](./data-lineage.mdx) — trace decisions back to source data +- [Agent-Safe Query Planning with Ontology First](/tutorials/agent-safe-query-planning) — run agents over this memory graph safely +- [Episodic Memory for Multi-Step Agents](/tutorials/episodic-memory) — session-scoped context alongside persistent team memory +- [Data Lineage](/tutorials/data-lineage) — trace decisions back to source data diff --git a/docs/docs/tutorials/episodic-memory.mdx b/docs/docs/learn/tutorials/agent-memory/episodic-memory.mdx similarity index 85% rename from docs/docs/tutorials/episodic-memory.mdx rename to docs/docs/learn/tutorials/agent-memory/episodic-memory.mdx index 579e2def..23e67cf9 100644 --- a/docs/docs/tutorials/episodic-memory.mdx +++ b/docs/docs/learn/tutorials/agent-memory/episodic-memory.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/episodic-memory sidebar_position: 23 -title: "Episodic Memory for Multi-Step Agents" +title: 'Episodic Memory for Multi-Step Agents' description: Store goals, intermediate observations, tool outputs, and decisions as linked records so long-running agents can resume with context instead of stateless prompts. tags: [Agents, Memory, Relationships, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Episodic Memory for Multi-Step Agents @@ -28,10 +29,10 @@ graph TD STEP_3 -->|DEPENDS_ON| STEP_1 ``` -| Label | What it represents | -|---|---| -| `GOAL` | The agent's top-level objective for a session | -| `STEP` | A single action the agent took (tool call, reasoning step, decision) | +| Label | What it represents | +| ------------- | --------------------------------------------------------------------------- | +| `GOAL` | The agent's top-level objective for a session | +| `STEP` | A single action the agent took (tool call, reasoning step, decision) | | `OBSERVATION` | The result or output of a step (tool output, search result, partial answer) | --- @@ -59,10 +60,7 @@ async function startSession(taskDescription: string, agentId: string) { return goal } -const goal = await startSession( - 'Research RushDB graph query patterns and summarize the top 5', - 'agent-001' -) +const goal = await startSession('Research RushDB graph query patterns and summarize the top 5', 'agent-001') console.log('Session goal ID:', goal.__id) ``` @@ -131,12 +129,7 @@ Each agent action — tool call, web search, or LLM reasoning step — becomes a ```typescript -async function recordStep( - goalId: string, - stepType: string, - action: string, - index: number -) { +async function recordStep(goalId: string, stepType: string, action: string, index: number) { const step = await db.records.create({ label: 'STEP', data: { @@ -163,12 +156,7 @@ async function recordStep( return step } -const step1 = await recordStep( - goal.__id, - 'tool_call', - 'Search documentation: graph traversal patterns', - 1 -) +const step1 = await recordStep(goal.__id, 'tool_call', 'Search documentation: graph traversal patterns', 1) ``` @@ -233,11 +221,7 @@ When a tool returns results, store the output as an OBSERVATION linked to the ST ```typescript -async function recordObservation( - stepId: string, - content: string, - observationType: string -) { +async function recordObservation(stepId: string, content: string, observationType: string) { const obs = await db.records.create({ label: 'OBSERVATION', data: { @@ -374,9 +358,9 @@ async function resumeSession(goalId: string) { return { goal: { task: goal.task, status: goal.status }, - completedSteps: steps.data.filter(s => s.status === 'completed').length, + completedSteps: steps.data.filter((s) => s.status === 'completed').length, totalSteps: steps.total, - observations: observations.data.map(o => o.content) + observations: observations.data.map((o) => o.content) } } @@ -497,11 +481,11 @@ curl -s -X PATCH "$BASE/records/$GOAL_ID" \ ## When to use episodic memory vs. team memory -| Pattern | Use case | -|---|---| -| Episodic memory (this tutorial) | Per-session agent context: goals, steps, tool outputs | -| Team memory ([Building Team Memory](./building-team-memory.mdx)) | Shared persistent knowledge: tickets, docs, decisions | -| Fact memory ([RushDB as a Memory Layer](./memory-layer.mdx)) | Long-lived facts about entities across all sessions | +| Pattern | Use case | +| --------------------------------------------------------------------- | ----------------------------------------------------- | +| Episodic memory (this tutorial) | Per-session agent context: goals, steps, tool outputs | +| Team memory ([Building Team Memory](/tutorials/building-team-memory)) | Shared persistent knowledge: tickets, docs, decisions | +| Fact memory ([RushDB as a Memory Layer](/tutorials/memory-layer)) | Long-lived facts about entities across all sessions | These patterns compose: an agent can write episodic steps to its own GOAL graph while also reading from shared team memory for background knowledge. @@ -515,6 +499,6 @@ Episodic records accumulate quickly during active agent use. Define a cleanup po ## Next steps -- [RushDB as a Memory Layer](./memory-layer.mdx) — long-lived facts and entity profiles -- [Building Team Memory](./building-team-memory.mdx) — shared knowledge graph for agents -- [Agent-Safe Query Planning](./agent-safe-query-planning.mdx) — grounded query execution guard +- [RushDB as a Memory Layer](/tutorials/memory-layer) — long-lived facts and entity profiles +- [Building Team Memory](/tutorials/building-team-memory) — shared knowledge graph for agents +- [Agent-Safe Query Planning](/tutorials/agent-safe-query-planning) — grounded query execution guard diff --git a/docs/docs/tutorials/memory-layer.mdx b/docs/docs/learn/tutorials/agent-memory/memory-layer.mdx similarity index 91% rename from docs/docs/tutorials/memory-layer.mdx rename to docs/docs/learn/tutorials/agent-memory/memory-layer.mdx index f5e32de1..810a348a 100644 --- a/docs/docs/tutorials/memory-layer.mdx +++ b/docs/docs/learn/tutorials/agent-memory/memory-layer.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/memory-layer sidebar_position: 11 -title: "RushDB as a Memory Layer: Facts, Episodes, and References" +title: 'RushDB as a Memory Layer: Facts, Episodes, and References' description: Model persistent facts, episodic interactions, and linked reference material as a graph so agents and applications can retrieve and reason over connected context. tags: [Agents, AI, Memory, Graph Modeling] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # RushDB as a Memory Layer: Facts, Episodes, and References @@ -14,13 +15,13 @@ Stateless LLM calls forget everything between turns. Retrieval from flat vector RushDB can act as a structured memory layer that stores and links three kinds of information: -| Memory type | What it stores | Example | -|---|---|---| -| **Fact** | Durable properties of entities | Customer's plan tier, user's language preference, a product's spec | -| **Episode** | Time-stamped interactions between entities | A support conversation, a search session, a tool call result | -| **Reference** | External documents or data linked into the graph | A knowledge-base article, a policy document, an API response | +| Memory type | What it stores | Example | +| ------------- | ------------------------------------------------ | ------------------------------------------------------------------ | +| **Fact** | Durable properties of entities | Customer's plan tier, user's language preference, a product's spec | +| **Episode** | Time-stamped interactions between entities | A support conversation, a search session, a tool call result | +| **Reference** | External documents or data linked into the graph | A knowledge-base article, a policy document, an API response | -Connecting all three lets an agent answer: *"What did we decide last time this user asked about pricing, and what docs were cited in that conversation?"* +Connecting all three lets an agent answer: _"What did we decide last time this user asked about pricing, and what docs were cited in that conversation?"_ --- @@ -79,7 +80,7 @@ const langFact = await db.records.create({ await Promise.all([ db.records.attach({ source: user, target: planFact, options: { type: 'HAS_FACT' } }), - db.records.attach({ source: user, target: langFact, options: { type: 'HAS_FACT' } }), + db.records.attach({ source: user, target: langFact, options: { type: 'HAS_FACT' } }) ]) ``` @@ -243,7 +244,7 @@ const pricingRef = await db.records.create({ await Promise.all([ db.records.attach({ source: pricingRef, target: kb, options: { type: 'PART_OF' } }), - db.records.attach({ source: episode, target: pricingRef, options: { type: 'CITED' } }), + db.records.attach({ source: episode, target: pricingRef, options: { type: 'CITED' } }) ]) ``` @@ -466,7 +467,7 @@ await Promise.all([ db.records.attach({ source: user, target: newPlanFact, options: { type: 'HAS_FACT' } }), db.records.attach({ source: upgradeEpisode, target: newPlanFact, options: { type: 'LED_TO' } }), // Mark old fact as superseded (optional but enables temporal queries) - db.records.update(planFact.__id, { supersededAt: new Date().toISOString() }), + db.records.update(planFact.__id, { supersededAt: new Date().toISOString() }) ]) ``` @@ -586,6 +587,6 @@ History-preserving fact graphs grow unbounded. Apply a retention policy: periodi ## Next steps -- [Episodic Memory for Multi-Step Agents](./episodic-memory) — storing intermediate observations, tool outputs, and decisions in a resume-capable graph -- [End-to-End Data Lineage](./data-lineage.mdx) — applying the same Event + Reference pattern to data pipeline provenance -- [Semantic Search for Multi-Tenant Products](./semantic-search-multitenant.mdx) — combining graph facts with vector retrieval +- [Episodic Memory for Multi-Step Agents](/tutorials/episodic-memory) — storing intermediate observations, tool outputs, and decisions in a resume-capable graph +- [End-to-End Data Lineage](/tutorials/data-lineage) — applying the same Event + Reference pattern to data pipeline provenance +- [Semantic Search for Multi-Tenant Products](/tutorials/semantic-search-multitenant) — combining graph facts with vector retrieval diff --git a/docs/docs/tutorials/research-knowledge-graph.mdx b/docs/docs/learn/tutorials/agent-memory/research-knowledge-graph.mdx similarity index 80% rename from docs/docs/tutorials/research-knowledge-graph.mdx rename to docs/docs/learn/tutorials/agent-memory/research-knowledge-graph.mdx index c60c3168..0ebeaf1b 100644 --- a/docs/docs/tutorials/research-knowledge-graph.mdx +++ b/docs/docs/learn/tutorials/agent-memory/research-knowledge-graph.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/research-knowledge-graph sidebar_position: 27 -title: "Research Knowledge Graph: Papers, Authors, Topics, Citations" +title: 'Research Knowledge Graph: Papers, Authors, Topics, Citations' description: Build a scholarly graph supporting citation traversal, topical clustering, and author-centric discovery for research workflows. tags: [Domain Blueprint, AI Search, Relationships, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Research Knowledge Graph: Papers, Authors, Topics, Citations @@ -27,12 +28,12 @@ graph LR TOPIC -->|RELATED_TO| TOPIC ``` -| Label | What it represents | -|---|---| -| `PAPER` | A research paper with title, abstract, year, DOI | -| `AUTHOR` | A researcher or collaborator | -| `INSTITUTION` | University, lab, or company | -| `TOPIC` | A subject area or keyword cluster | +| Label | What it represents | +| ------------- | ------------------------------------------------ | +| `PAPER` | A research paper with title, abstract, year, DOI | +| `AUTHOR` | A researcher or collaborator | +| `INSTITUTION` | University, lab, or company | +| `TOPIC` | A subject area or keyword cluster | --- @@ -52,7 +53,8 @@ await db.records.importJson({ { doi: '10.1000/xyz001', title: 'Graph Databases for Scientific Knowledge Representation', - abstract: 'This paper surveys the use of graph databases in representing and querying scientific knowledge, including citation networks, ontologies, and experimental results.', + abstract: + 'This paper surveys the use of graph databases in representing and querying scientific knowledge, including citation networks, ontologies, and experimental results.', year: 2023, venue: 'VLDB', citationCount: 47 @@ -60,7 +62,8 @@ await db.records.importJson({ { doi: '10.1000/xyz002', title: 'Neural Retrieval Augmentation with Knowledge Graphs', - abstract: 'We propose a retrieval augmentation framework that combines dense vector search with structured graph traversal to improve factual precision in language model outputs.', + abstract: + 'We propose a retrieval augmentation framework that combines dense vector search with structured graph traversal to improve factual precision in language model outputs.', year: 2024, venue: 'NeurIPS', citationCount: 112 @@ -68,7 +71,8 @@ await db.records.importJson({ { doi: '10.1000/xyz003', title: 'Scalable Graph Construction from Unstructured Text', - abstract: 'A pipeline for extracting entities and relationships from scientific text and constructing queryable knowledge graphs at scale.', + abstract: + 'A pipeline for extracting entities and relationships from scientific text and constructing queryable knowledge graphs at scale.', year: 2024, venue: 'ACL', citationCount: 29 @@ -180,23 +184,55 @@ const [papers, authors, topics] = await Promise.all([ db.records.find({ labels: ['TOPIC'] }) ]) -const paperMap = Object.fromEntries(papers.data.map(p => [p.doi, p])) -const authorMap = Object.fromEntries(authors.data.map(a => [a.email, a])) -const topicMap = Object.fromEntries(topics.data.map(t => [t.name, t])) +const paperMap = Object.fromEntries(papers.data.map((p) => [p.doi, p])) +const authorMap = Object.fromEntries(authors.data.map((a) => [a.email, a])) +const topicMap = Object.fromEntries(topics.data.map((t) => [t.name, t])) // Paper xyz001: authored by Yuki Tanaka and Lena Müller -await db.records.attach({ source: paperMap['10.1000/xyz001'], target: authorMap['y.tanaka@uni.edu'], options: { type: 'AUTHORED_BY', direction: 'out' } }) -await db.records.attach({ source: paperMap['10.1000/xyz001'], target: authorMap['l.muller@institute.de'], options: { type: 'AUTHORED_BY', direction: 'out' } }) +await db.records.attach({ + source: paperMap['10.1000/xyz001'], + target: authorMap['y.tanaka@uni.edu'], + options: { type: 'AUTHORED_BY', direction: 'out' } +}) +await db.records.attach({ + source: paperMap['10.1000/xyz001'], + target: authorMap['l.muller@institute.de'], + options: { type: 'AUTHORED_BY', direction: 'out' } +}) // Paper xyz002: authored by Lena Müller and Carlos Reyes, cites xyz001 -await db.records.attach({ source: paperMap['10.1000/xyz002'], target: authorMap['l.muller@institute.de'], options: { type: 'AUTHORED_BY', direction: 'out' } }) -await db.records.attach({ source: paperMap['10.1000/xyz002'], target: authorMap['c.reyes@lab.com'], options: { type: 'AUTHORED_BY', direction: 'out' } }) -await db.records.attach({ source: paperMap['10.1000/xyz002'], target: paperMap['10.1000/xyz001'], options: { type: 'CITES', direction: 'out' } }) +await db.records.attach({ + source: paperMap['10.1000/xyz002'], + target: authorMap['l.muller@institute.de'], + options: { type: 'AUTHORED_BY', direction: 'out' } +}) +await db.records.attach({ + source: paperMap['10.1000/xyz002'], + target: authorMap['c.reyes@lab.com'], + options: { type: 'AUTHORED_BY', direction: 'out' } +}) +await db.records.attach({ + source: paperMap['10.1000/xyz002'], + target: paperMap['10.1000/xyz001'], + options: { type: 'CITES', direction: 'out' } +}) // Topics -await db.records.attach({ source: paperMap['10.1000/xyz001'], target: topicMap['graph databases'], options: { type: 'COVERS', direction: 'out' } }) -await db.records.attach({ source: paperMap['10.1000/xyz002'], target: topicMap['retrieval augmented generation'], options: { type: 'COVERS', direction: 'out' } }) -await db.records.attach({ source: paperMap['10.1000/xyz002'], target: topicMap['knowledge representation'], options: { type: 'COVERS', direction: 'out' } }) +await db.records.attach({ + source: paperMap['10.1000/xyz001'], + target: topicMap['graph databases'], + options: { type: 'COVERS', direction: 'out' } +}) +await db.records.attach({ + source: paperMap['10.1000/xyz002'], + target: topicMap['retrieval augmented generation'], + options: { type: 'COVERS', direction: 'out' } +}) +await db.records.attach({ + source: paperMap['10.1000/xyz002'], + target: topicMap['knowledge representation'], + options: { type: 'COVERS', direction: 'out' } +}) ``` @@ -288,7 +324,10 @@ const coAuthors = await db.records.find({ } }) -console.log('Co-authors:', coAuthors.data.map(a => a.name)) +console.log( + 'Co-authors:', + coAuthors.data.map((a) => a.name) +) ``` @@ -375,7 +414,7 @@ let ready = false while (!ready) { const stats = await db.ai.indexes.stats('PAPER') ready = stats.data.indexedRecords === stats.data.totalRecords - if (!ready) await new Promise(r => setTimeout(r, 2000)) + if (!ready) await new Promise((r) => setTimeout(r, 2000)) } // Semantic search — hybrid: conceptual query + structured year filter @@ -446,6 +485,6 @@ Citation graphs become highly connected over time. Deep traversal queries (paper ## Next steps -- [Hybrid Retrieval](./hybrid-retrieval.mdx) — filter + semantic rank in one call -- [Modeling Hierarchies, Networks, and Feedback Loops](./modeling-hierarchies.mdx) — structural patterns for citation networks -- [Data Lineage](./data-lineage.mdx) — trace derived knowledge back to source papers +- [Hybrid Retrieval](/tutorials/hybrid-retrieval) — filter + semantic rank in one call +- [Modeling Hierarchies, Networks, and Feedback Loops](/tutorials/modeling-hierarchies) — structural patterns for citation networks +- [Data Lineage](/tutorials/data-lineage) — trace derived knowledge back to source papers diff --git a/docs/docs/tutorials/ai-semantic-search.mdx b/docs/docs/learn/tutorials/ai-and-rag/ai-semantic-search.mdx similarity index 90% rename from docs/docs/tutorials/ai-semantic-search.mdx rename to docs/docs/learn/tutorials/ai-and-rag/ai-semantic-search.mdx index e8dbe4fd..f9364192 100644 --- a/docs/docs/tutorials/ai-semantic-search.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/ai-semantic-search.mdx @@ -1,16 +1,18 @@ --- +slug: /tutorials/ai-semantic-search sidebar_position: 5 title: Semantic Search in 5 Minutes description: Create embedding indexes, wait for backfill, and run your first semantic search query in TypeScript, Python, or REST. tags: [AI, Search] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Semantic Search in 5 Minutes This tutorial walks you through the full semantic search workflow: + 1. Push some records 2. Create an embedding index on a text property 3. Poll until the index is ready @@ -161,6 +163,7 @@ token: YOUR_API_KEY ``` Response: + ```json { "data": { @@ -193,7 +196,7 @@ async function waitForIndex(indexId: string, intervalMs = 2000) { const { data: stats } = await db.ai.indexes.stats(indexId) console.log(`${stats.indexedRecords} / ${stats.totalRecords} embedded`) if (stats.indexedRecords >= stats.totalRecords && stats.totalRecords > 0) break - await new Promise(r => setTimeout(r, intervalMs)) + await new Promise((r) => setTimeout(r, intervalMs)) } } @@ -296,9 +299,27 @@ token: YOUR_API_KEY ```json { "data": [ - { "__id": "rec_1", "__label": "Article", "__score": 0.921, "title": "Intro to Machine Learning", "description": "..." }, - { "__id": "rec_2", "__label": "Article", "__score": 0.743, "title": "Graph Databases Explained", "description": "..." }, - { "__id": "rec_3", "__label": "Article", "__score": 0.612, "title": "Climate Science Overview", "description": "..." } + { + "__id": "rec_1", + "__label": "Article", + "__score": 0.921, + "title": "Intro to Machine Learning", + "description": "..." + }, + { + "__id": "rec_2", + "__label": "Article", + "__score": 0.743, + "title": "Graph Databases Explained", + "description": "..." + }, + { + "__id": "rec_3", + "__label": "Article", + "__score": 0.612, + "title": "Climate Science Overview", + "description": "..." + } ], "success": true } @@ -377,6 +398,6 @@ token: YOUR_API_KEY ## Next steps -- **Inject schema context into an LLM**: [TypeScript AI docs](../typescript-sdk/ai/overview) | [Python AI docs](../python-sdk/ai/overview) -- **REST API reference**: [REST AI docs](../rest-api/ai/overview) +- **Inject schema context into an LLM**: [TypeScript AI docs](/build/ai-search/semantic-search) | [Python AI docs](/build/ai-search/semantic-search) +- **REST API reference**: [REST AI docs](/rest-api/ai/overview) - **Multiple labels**: Pass 2+ labels in `labels` to search across all specified labels. diff --git a/docs/docs/tutorials/byov-external-embeddings.mdx b/docs/docs/learn/tutorials/ai-and-rag/byov-external-embeddings.mdx similarity index 79% rename from docs/docs/tutorials/byov-external-embeddings.mdx rename to docs/docs/learn/tutorials/ai-and-rag/byov-external-embeddings.mdx index 36a01bed..f848be7e 100644 --- a/docs/docs/tutorials/byov-external-embeddings.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/byov-external-embeddings.mdx @@ -1,18 +1,20 @@ --- +slug: /tutorials/byov-external-embeddings sidebar_position: 35 -title: "Bring Your Own Vectors (BYOV) — External Embeddings" +title: 'Bring Your Own Vectors (BYOV) — External Embeddings' description: Use your own embedding model to generate vectors and store them in RushDB, then search with queryVector instead of query text. tags: [AI, BYOV, Embeddings, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Bring Your Own Vectors (BYOV) — External Embeddings By default, RushDB generates embeddings for you using the configured server-side embedding model. With BYOV you generate the vectors yourself — using any model, any provider, even a locally fine-tuned one — and push them alongside your records. RushDB stores and indexes them, and you search with a pre-computed `queryVector` instead of a raw query string. Use BYOV when: + - You have a fine-tuned or domain-specific embedding model - Your compliance requirements prohibit sending raw text to a third-party embedding API - You want to co-locate embedding cost with your own infrastructure @@ -21,13 +23,13 @@ Use BYOV when: ## How it differs from managed embeddings -| | Managed | BYOV (external) | -|---|---|---| -| Who generates vectors | RushDB (server-side) | You (client-side) | -| Search parameter | `query: "text"` | `queryVector: number[]` | -| Index `sourceType` | `managed` (default) | `external` | -| Shorthand | — | `external: true` | -| Dimensions | Set by server `RUSHDB_EMBEDDING_DIMENSIONS` | Set per index by you | +| | Managed | BYOV (external) | +| --------------------- | ------------------------------------------- | ----------------------- | +| Who generates vectors | RushDB (server-side) | You (client-side) | +| Search parameter | `query: "text"` | `queryVector: number[]` | +| Index `sourceType` | `managed` (default) | `external` | +| Shorthand | — | `external: true` | +| Dimensions | Set by server `RUSHDB_EMBEDDING_DIMENSIONS` | Set per index by you | Both index types can coexist on the same label and property. @@ -46,9 +48,9 @@ const db = new RushDB(process.env.RUSHDB_API_KEY!) await db.ai.indexes.create({ label: 'ARTICLE', propertyName: 'body', - external: true, // shorthand for sourceType: 'external' + external: true, // shorthand for sourceType: 'external' similarityFunction: 'cosine', - dimensions: 1536 // must match your model's output dimensions + dimensions: 1536 // must match your model's output dimensions }) ``` @@ -105,7 +107,7 @@ Use the `vectors` parameter on `create` (or `createMany` for batches) to deliver async function embed(text: string): Promise { const res = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', - headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' }, + headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'text-embedding-3-small', input: text }) }) const json = await res.json() @@ -114,7 +116,7 @@ async function embed(text: string): Promise { const articles = [ { title: 'Intro to Graph Databases', body: 'Graph databases store data as nodes and edges...' }, - { title: 'Vector Search Explained', body: 'Vector search finds semantically similar documents...' } + { title: 'Vector Search Explained', body: 'Vector search finds semantically similar documents...' } ] // Option A: batch import with createMany @@ -128,7 +130,7 @@ for (const article of articles) { await db.records.create({ label: 'ARTICLE', data: article, - vectors: [{ propertyName: 'body', vector: await embed(article.body) }], + vectors: [{ propertyName: 'body', vector: await embed(article.body) }] }) } ``` @@ -201,7 +203,7 @@ const queryVector = await embed('how do graph databases handle relationships?') const results = await db.ai.search({ propertyName: 'body', - queryVector, // dimensions inferred from queryVector.length + queryVector, // dimensions inferred from queryVector.length labels: ['ARTICLE'], limit: 5 }) @@ -303,7 +305,7 @@ If you create both a `cosine` and `euclidean` index on the same label+property, ```typescript vectors: [ - { propertyName: 'body', similarityFunction: 'cosine', vector: cosineEmbedding }, + { propertyName: 'body', similarityFunction: 'cosine', vector: cosineEmbedding }, { propertyName: 'body', similarityFunction: 'euclidean', vector: euclideanEmbedding } ] ``` @@ -331,17 +333,17 @@ vectors=[ -| Vector array entries | Matching indexes | Outcome | -|---|---|---| -| 1 entry, no `similarityFunction` | 1 match | OK — writes to that index | -| 1 entry, no `similarityFunction` | 2 matches | `422 Ambiguous — specify similarityFunction` | -| 1 entry, no `similarityFunction` | 0 matches | `404 No external index found` | -| 1 entry, with `similarityFunction` | — | Matched by both dimension + function | +| Vector array entries | Matching indexes | Outcome | +| ---------------------------------- | ---------------- | -------------------------------------------- | +| 1 entry, no `similarityFunction` | 1 match | OK — writes to that index | +| 1 entry, no `similarityFunction` | 2 matches | `422 Ambiguous — specify similarityFunction` | +| 1 entry, no `similarityFunction` | 0 matches | `404 No external index found` | +| 1 entry, with `similarityFunction` | — | Matched by both dimension + function | --- ## Next steps -- [Hybrid Retrieval](./hybrid-retrieval.mdx) — `where` filters + semantic ranking in one call -- [GraphRAG](./graphrag.mdx) — use BYOV embeddings in a retrieval-augmented generation pipeline with graph traversal -- [RAG Evaluation](./rag-evaluation.mdx) — benchmark precision before and after swapping embedding models +- [Hybrid Retrieval](/tutorials/hybrid-retrieval) — `where` filters + semantic ranking in one call +- [GraphRAG](/tutorials/graphrag) — use BYOV embeddings in a retrieval-augmented generation pipeline with graph traversal +- [RAG Evaluation](/tutorials/rag-evaluation) — benchmark precision before and after swapping embedding models diff --git a/docs/docs/tutorials/byov-when-and-why.mdx b/docs/docs/learn/tutorials/ai-and-rag/byov-when-and-why.mdx similarity index 97% rename from docs/docs/tutorials/byov-when-and-why.mdx rename to docs/docs/learn/tutorials/ai-and-rag/byov-when-and-why.mdx index 6693b78c..a0d0e4cf 100644 --- a/docs/docs/tutorials/byov-when-and-why.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/byov-when-and-why.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/byov-when-and-why sidebar_position: 36 title: 'BYOV in Practice: When and Why to Bring Your Own Vectors' description: A case study showing exactly when BYOV makes sense, when it doesn't, and a complete walkthrough of a real scenario where you already have vectors from your own pipeline. @@ -230,7 +231,7 @@ Managed embeddings are the right default for most projects. BYOV is the escape h ## Further reading -- [BYOV concept page](/concepts/bring-your-own-vectors) — the what and how +- [BYOV concept page](/build/ai-search/bring-your-own-vectors) — the what and how - [BYOV External Embeddings tutorial](/tutorials/byov-external-embeddings) — step-by-step with TypeScript, Python, and Shell - [REST API: Advanced Indexing](/rest-api/ai/advanced-indexing) — full BYOV API reference including `upsertVectors` -- [Semantic Search concept](/concepts/semantic-search) — managed vs. external comparison +- [Semantic Search concept](/build/ai-search/semantic-search) — managed vs. external comparison diff --git a/docs/docs/tutorials/explainable-results.mdx b/docs/docs/learn/tutorials/ai-and-rag/explainable-results.mdx similarity index 88% rename from docs/docs/tutorials/explainable-results.mdx rename to docs/docs/learn/tutorials/ai-and-rag/explainable-results.mdx index c414aef0..da23b1ea 100644 --- a/docs/docs/tutorials/explainable-results.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/explainable-results.mdx @@ -1,16 +1,17 @@ --- +slug: /tutorials/explainable-results sidebar_position: 33 -title: "Explainable Results" +title: 'Explainable Results' description: Pair raw search results with related evidence, summary metrics, and traversal paths so users and agents can understand why a result was returned. tags: [Search, AI, SearchQuery, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Explainable Results -A result without context forces users to trust it blindly. An explainable result shows *why* it was returned: which fields matched, which related records reinforce it, and what summary metric supports it. +A result without context forces users to trust it blindly. An explainable result shows _why_ it was returned: which fields matched, which related records reinforce it, and what summary metric supports it. This tutorial shows three patterns for building explainable results on top of RushDB: @@ -52,7 +53,7 @@ function explainMatch(record: Record, query: Record) { const result = await db.records.find({ labels: ['ARTICLE'], where }) - return result.data.map(record => ({ + return result.data.map((record) => ({ id: record.__id, title: record.title, matchedFields: explainMatch(record as Record, where) @@ -155,10 +156,11 @@ async function assembleEvidence(articleId: string): Promise t.name as string), + relatedTopics: topicResult.data.map((t) => t.name as string), citedBy: (citedByResult.data[0]?.count as number) ?? 0 } } @@ -173,7 +175,7 @@ async function explainedSemanticSearch(userQuery: string): Promise ({ + results.data.map(async (article) => ({ id: article.__id, title: article.title as string, score: article.__score as number, @@ -274,7 +276,9 @@ function buildExplanationText(result: { } if (result.evidence.citedBy > 0) { - parts.push(`Cited by ${result.evidence.citedBy} other article${result.evidence.citedBy === 1 ? '' : 's'}.`) + parts.push( + `Cited by ${result.evidence.citedBy} other article${result.evidence.citedBy === 1 ? '' : 's'}.` + ) } return parts.join(' ') @@ -345,7 +349,9 @@ const authorResult = await db.records.find({ } }) -console.log(`Result: ${article.title} (score: ${article.__score}) — Author: ${authorResult.data[0]?.name ?? 'unknown'}`) +console.log( + `Result: ${article.title} (score: ${article.__score}) — Author: ${authorResult.data[0]?.name ?? 'unknown'}` +) ``` @@ -410,12 +416,12 @@ echo "Result: $TITLE (score: $SCORE) — Author: $AUTHOR" ## When to use explainability -| Context | Recommended patterns | -|---|---| -| User-facing search results | Field match highlights + related topic tags | -| AI agent tool responses | Score + structured summary text | +| Context | Recommended patterns | +| --------------------------- | -------------------------------------------- | +| User-facing search results | Field match highlights + related topic tags | +| AI agent tool responses | Score + structured summary text | | Compliance / audit surfaces | Full evidence assembly with provenance links | -| Debug / development | Field-level match + raw score logging | +| Debug / development | Field-level match + raw score logging | --- @@ -427,6 +433,6 @@ Evidence assembly makes one additional query per result per evidence type. For 1 ## Next steps -- [Search UX Patterns](./search-ux-patterns.mdx) — structured and semantic search for the end user -- [Hybrid Retrieval](./hybrid-retrieval.mdx) — `where` filter + semantic scoring in one call -- [Research Knowledge Graph](./research-knowledge-graph.mdx) — citation and co-authorship graphs to power evidence traversal +- [Search UX Patterns](/tutorials/search-ux-patterns) — structured and semantic search for the end user +- [Hybrid Retrieval](/tutorials/hybrid-retrieval) — `where` filter + semantic scoring in one call +- [Research Knowledge Graph](/tutorials/research-knowledge-graph) — citation and co-authorship graphs to power evidence traversal diff --git a/docs/docs/tutorials/graphrag.mdx b/docs/docs/learn/tutorials/ai-and-rag/graphrag.mdx similarity index 83% rename from docs/docs/tutorials/graphrag.mdx rename to docs/docs/learn/tutorials/ai-and-rag/graphrag.mdx index f01c4ff5..b6d9e309 100644 --- a/docs/docs/tutorials/graphrag.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/graphrag.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/graphrag sidebar_position: 36 -title: "GraphRAG — Graph-Enriched Retrieval Augmented Generation" +title: 'GraphRAG — Graph-Enriched Retrieval Augmented Generation' description: Retrieve chunks semantically, then traverse the knowledge graph to assemble author, topic, and source provenance as richer LLM context. tags: [AI, RAG, GraphRAG, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # GraphRAG — Graph-Enriched Retrieval Augmented Generation @@ -36,12 +37,12 @@ graph LR CHUNK -->|RELATED_TO| CHUNK2[CHUNK
related chunk] ``` -| Label | What it represents | -|---|---| +| Label | What it represents | +| -------- | ---------------------------------------------------------- | | `SOURCE` | A document, web page, or data export (the original source) | -| `CHUNK` | A text fragment from a source, with overlap | -| `AUTHOR` | A person or team that authored the source | -| `TOPIC` | A concept tag associated with a chunk | +| `CHUNK` | A text fragment from a source, with overlap | +| `AUTHOR` | A person or team that authored the source | +| `TOPIC` | A concept tag associated with a chunk | --- @@ -69,31 +70,42 @@ function chunkText(text: string): string[] { return chunks.filter(Boolean) } -interface SourceMeta { filename: string; author: string; topics: string[] } +interface SourceMeta { + filename: string + author: string + topics: string[] +} async function ingestSource(meta: SourceMeta, content: string) { const tx = await db.tx.begin() try { // Create the SOURCE record - const source = await db.records.create({ - label: 'SOURCE', - data: { filename: meta.filename, ingestedAt: new Date().toISOString() } - }, tx) + const source = await db.records.create( + { + label: 'SOURCE', + data: { filename: meta.filename, ingestedAt: new Date().toISOString() } + }, + tx + ) // Create AUTHOR if not already present (find-then-create) const existingAuthor = await db.records.find({ labels: ['AUTHOR'], where: { name: meta.author } }) - const author = existingAuthor.data.length > 0 - ? existingAuthor.data[0] + const author = + existingAuthor.data.length > 0 ? + existingAuthor.data[0] : await db.records.create({ label: 'AUTHOR', data: { name: meta.author } }, tx) - await db.records.attach({ source: author, target: source, options: { type: 'AUTHORED', direction: 'out' } }, tx) + await db.records.attach( + { source: author, target: source, options: { type: 'AUTHORED', direction: 'out' } }, + tx + ) // Create chunks and link to source const texts = chunkText(content) const chunks = await db.records.importJson({ label: 'CHUNK', data: texts.map((text, i) => ({ text, chunkIndex: i, sourceFile: meta.filename })) - }) // Note: importJson doesn't support tx — do in a follow-up pass for link + }) // Note: importJson doesn't support tx — do in a follow-up pass for link // Link chunks to source (outside transaction — importJson is atomic on its own) await db.tx.commit(tx) @@ -112,11 +124,16 @@ async function ingestSource(meta: SourceMeta, content: string) { // Attach topics for (const topicName of meta.topics) { const existingTopic = await db.records.find({ labels: ['TOPIC'], where: { name: topicName } }) - const topic = existingTopic.data.length > 0 - ? existingTopic.data[0] + const topic = + existingTopic.data.length > 0 ? + existingTopic.data[0] : await db.records.create({ label: 'TOPIC', data: { name: topicName } }) for (const chunk of chunkRecords.data) { - await db.records.attach({ source: topic, target: chunk, options: { type: 'COVERS', direction: 'out' } }) + await db.records.attach({ + source: topic, + target: chunk, + options: { type: 'COVERS', direction: 'out' } + }) } } @@ -207,7 +224,7 @@ await db.ai.indexes.create({ label: 'CHUNK', propertyName: 'text' }) // Poll until ready let stats = await db.ai.indexes.stats('your-index-id') while (stats.data.indexedRecords < stats.data.totalRecords) { - await new Promise(r => setTimeout(r, 3000)) + await new Promise((r) => setTimeout(r, 3000)) stats = await db.ai.indexes.stats('your-index-id') } console.log('Index ready') @@ -283,8 +300,9 @@ async function graphRagRetrieve(userQuery: string, k = 5): Promise t.name as string), + source: (source?.filename as string) ?? 'unknown', + author: (authorResult.data[0]?.name as string) ?? null, + topics: topicResult.data.map((t) => t.name as string), score: chunk.__score as number } }) @@ -307,11 +325,15 @@ async function graphRagRetrieve(userQuery: string, k = 5): Promise [ - `[${i + 1}] (score: ${c.score.toFixed(2)}, source: ${c.source}, author: ${c.author ?? 'unknown'})`, - `Topics: ${c.topics.join(', ') || 'none'}`, - c.text - ].join('\n')).join('\n\n---\n\n') + const contextBlocks = chunks + .map((c, i) => + [ + `[${i + 1}] (score: ${c.score.toFixed(2)}, source: ${c.source}, author: ${c.author ?? 'unknown'})`, + `Topics: ${c.topics.join(', ') || 'none'}`, + c.text + ].join('\n') + ) + .join('\n\n---\n\n') return [ 'You are a helpful assistant. Answer using the provided context.', @@ -400,12 +422,14 @@ print(prompt) ## GraphRAG vs flat RAG — what changes in the prompt **Flat RAG prompt fragment:** + ``` [1] The cache layer uses a time-based TTL of 300 seconds. Stale entries are invalidated on next read by comparing the stored timestamp... ``` **GraphRAG prompt fragment:** + ``` [1] (score: 0.91, source: architecture.md, author: Jane Smith) Topics: distributed systems, caching @@ -413,7 +437,7 @@ The cache layer uses a time-based TTL of 300 seconds. Stale entries are invalidated on next read by comparing the stored timestamp... ``` -The LLM now knows *where this knowledge came from*, *who wrote it*, and *what domain it belongs to*. This enables citation-aware answers and reduces hallucination on ambiguous questions. +The LLM now knows _where this knowledge came from_, _who wrote it_, and _what domain it belongs to_. This enables citation-aware answers and reduces hallucination on ambiguous questions. --- @@ -425,6 +449,6 @@ Each retrieved chunk triggers two additional queries for source and topic enrich ## Next steps -- [BYOV External Embeddings](./byov-external-embeddings.mdx) — supply your own vectors instead of relying on managed embeddings -- [Multi-Source RAG](./rag-multi-source.mdx) — combine PDFs, web pages, and database exports in one semantic search -- [RAG Evaluation](./rag-evaluation.mdx) — measure precision and recall before deploying GraphRAG to production +- [BYOV External Embeddings](/tutorials/byov-external-embeddings) — supply your own vectors instead of relying on managed embeddings +- [Multi-Source RAG](/tutorials/rag-multi-source) — combine PDFs, web pages, and database exports in one semantic search +- [RAG Evaluation](/tutorials/rag-evaluation) — measure precision and recall before deploying GraphRAG to production diff --git a/docs/docs/tutorials/hybrid-retrieval.mdx b/docs/docs/learn/tutorials/ai-and-rag/hybrid-retrieval.mdx similarity index 91% rename from docs/docs/tutorials/hybrid-retrieval.mdx rename to docs/docs/learn/tutorials/ai-and-rag/hybrid-retrieval.mdx index a766f483..bc3b9656 100644 --- a/docs/docs/tutorials/hybrid-retrieval.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/hybrid-retrieval.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/hybrid-retrieval sidebar_position: 19 -title: "Hybrid Retrieval: Filters Plus Semantic Search" +title: 'Hybrid Retrieval: Filters Plus Semantic Search' description: Combine structured where-clause filtering with vector semantic search to narrow candidates by business constraints, then rank by relevance. tags: [AI, Semantic Search, Retrieval, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Hybrid Retrieval: Filters Plus Semantic Search @@ -30,7 +31,7 @@ This means you get tenant isolation and time-scoping for free, without a separat ## Prerequisites -Before calling `db.ai.search`, a managed embedding index must exist and be ready for the label and property you want to search. See [Semantic Search for Multi-Tenant Products](./semantic-search-multitenant.mdx) for setup instructions. +Before calling `db.ai.search`, a managed embedding index must exist and be ready for the label and property you want to search. See [Semantic Search for Multi-Tenant Products](/tutorials/semantic-search-multitenant) for setup instructions. --- @@ -345,7 +346,7 @@ async function searchArticles(query: string, tenantId: string, page = 0, pageSiz }) return { - results: results.data.map(r => ({ + results: results.data.map((r) => ({ id: r.__id, title: r.title, score: r.__score @@ -529,14 +530,14 @@ curl -s -X POST "$BASE/ai/search" \ ## When to use pure search vs. hybrid -| Scenario | Approach | -|---|---| -| Open-ended exploration ("find anything about X") | Pure semantic search, no `where` | -| Tenant-scoped retrieval | Always add `tenantId` to `where` | -| Time-bounded retrieval (last 30 days) | Add date filter to `where` | -| Category/status filtering before ranking | Add structured fields to `where` | +| Scenario | Approach | +| ----------------------------------------------------- | ------------------------------------- | +| Open-ended exploration ("find anything about X") | Pure semantic search, no `where` | +| Tenant-scoped retrieval | Always add `tenantId` to `where` | +| Time-bounded retrieval (last 30 days) | Add date filter to `where` | +| Category/status filtering before ranking | Add structured fields to `where` | | Graph-relationship scoping (tagged with, authored by) | Add relationship traversal to `where` | -| Custom embedding model | Use `queryVector` instead of `query` | +| Custom embedding model | Use `queryVector` instead of `query` | --- @@ -550,6 +551,6 @@ curl -s -X POST "$BASE/ai/search" \ ## Next steps -- [Semantic Search for Multi-Tenant Products](./semantic-search-multitenant.mdx) — index setup and polling -- [Building a Graph-Backed API Layer](./graph-backed-api.mdx) — integrate hybrid retrieval into a production handler -- [Temporal Graphs](./temporal-graphs.mdx) — retrieve current-state records before semantic ranking +- [Semantic Search for Multi-Tenant Products](/tutorials/semantic-search-multitenant) — index setup and polling +- [Building a Graph-Backed API Layer](/tutorials/graph-backed-api) — integrate hybrid retrieval into a production handler +- [Temporal Graphs](/tutorials/temporal-graphs) — retrieve current-state records before semantic ranking diff --git a/docs/docs/tutorials/rag-evaluation.mdx b/docs/docs/learn/tutorials/ai-and-rag/rag-evaluation.mdx similarity index 80% rename from docs/docs/tutorials/rag-evaluation.mdx rename to docs/docs/learn/tutorials/ai-and-rag/rag-evaluation.mdx index 44816e0b..6755b78f 100644 --- a/docs/docs/tutorials/rag-evaluation.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/rag-evaluation.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/rag-evaluation sidebar_position: 40 -title: "RAG Evaluation" +title: 'RAG Evaluation' description: Measure Precision@k and Recall@k for your retrieval pipeline, detect score drift after model updates, and add a CI regression gate that fails on quality drops. tags: [RAG, AI, Embeddings, Testing] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # RAG Evaluation @@ -16,10 +17,10 @@ Retrieval quality degrades silently: a new embedding model bumps up dimensions, ## Concepts -| Metric | Definition | Formula | -|---|---|---| -| **Precision@k** | Of the k results returned, how many are relevant? | `|retrieved ∩ relevant| / k` | -| **Recall@k** | Of all relevant items, how many appear in the top k? | `|retrieved ∩ relevant| / |relevant|` | +| Metric | Definition | Formula | +| --------------- | ---------------------------------------------------- | ------- | -------------------- | ---- | -------- | --- | +| **Precision@k** | Of the k results returned, how many are relevant? | ` | retrieved ∩ relevant | / k` | +| **Recall@k** | Of all relevant items, how many appear in the top k? | ` | retrieved ∩ relevant | / | relevant | ` | A well-tuned RAG retriever should maintain **Precision@5 ≥ 0.6** and **Recall@5 ≥ 0.5** for your domain. The exact thresholds depend on your corpus — establish a baseline first, then track drift. @@ -37,26 +38,15 @@ Create a small set of evaluation queries paired with expected top record IDs. Ai export const evalDataset = [ { query: 'how to set up a self-hosted RushDB instance', - expectedIds: [ - 'record-id-docker-guide', - 'record-id-env-vars', - 'record-id-first-boot' - ] + expectedIds: ['record-id-docker-guide', 'record-id-env-vars', 'record-id-first-boot'] }, { query: 'connecting a Neo4j Aura database', - expectedIds: [ - 'record-id-aura-setup', - 'record-id-bolt-url' - ] + expectedIds: ['record-id-aura-setup', 'record-id-bolt-url'] }, { query: 'external embedding indexes BYOV', - expectedIds: [ - 'record-id-byov-intro', - 'record-id-external-index', - 'record-id-vectors-key' - ] + expectedIds: ['record-id-byov-intro', 'record-id-external-index', 'record-id-vectors-key'] } // Add 20–50 entries for statistical significance ] @@ -116,7 +106,7 @@ const hits = await db.ai.search({ limit: 10 }) -hits.forEach(h => console.log(h.__id, h.__score?.toFixed(3), h.text.slice(0, 80))) +hits.forEach((h) => console.log(h.__id, h.__score?.toFixed(3), h.text.slice(0, 80))) // Manually note which IDs are relevant → add to evalDataset ``` @@ -156,7 +146,7 @@ for h in hits: export function precisionAtK(retrievedIds, relevantIds, k) { const topK = retrievedIds.slice(0, k) const relevantSet = new Set(relevantIds) - const hits = topK.filter(id => relevantSet.has(id)).length + const hits = topK.filter((id) => relevantSet.has(id)).length return hits / k } @@ -164,7 +154,7 @@ export function recallAtK(retrievedIds, relevantIds, k) { if (relevantIds.length === 0) return 1 const topK = retrievedIds.slice(0, k) const relevantSet = new Set(relevantIds) - const hits = topK.filter(id => relevantSet.has(id)).length + const hits = topK.filter((id) => relevantSet.has(id)).length return hits / relevantIds.length } @@ -227,7 +217,7 @@ async function evaluate() { limit: K }) - const retrievedIds = hits.map(h => h.__id) + const retrievedIds = hits.map((h) => h.__id) precisions.push(precisionAtK(retrievedIds, expectedIds, K)) recalls.push(recallAtK(retrievedIds, expectedIds, K)) } @@ -331,7 +321,7 @@ async function captureScoreSnapshot(db, queries) { propertyName: 'text', limit: 5 }) - snapshot[query] = hits.map(h => ({ id: h.__id, score: h.__score })) + snapshot[query] = hits.map((h) => ({ id: h.__id, score: h.__score })) } fs.writeFileSync(SNAPSHOT_FILE, JSON.stringify(snapshot, null, 2)) console.log('Score snapshot saved.') @@ -342,7 +332,7 @@ function compareSnapshots(baseline, current, driftThreshold = 0.05) { const drifts = [] for (const query of Object.keys(baseline)) { if (!current[query]) continue - const baseScores = Object.fromEntries(baseline[query].map(r => [r.id, r.score])) + const baseScores = Object.fromEntries(baseline[query].map((r) => [r.id, r.score])) for (const { id, score } of current[query]) { if (id in baseScores) { const delta = Math.abs(score - baseScores[id]) @@ -420,31 +410,33 @@ Plug the evaluation harness into your CI pipeline so merges that degrade retriev // eval.ci.js — run with: node eval.ci.js import { evaluate } from './evaluate.js' -const PRECISION_THRESHOLD = 0.60 -const RECALL_THRESHOLD = 0.50 +const PRECISION_THRESHOLD = 0.6 +const RECALL_THRESHOLD = 0.5 const results = await evaluate() -const passed = - results.meanPrecision >= PRECISION_THRESHOLD && - results.meanRecall >= RECALL_THRESHOLD +const passed = results.meanPrecision >= PRECISION_THRESHOLD && results.meanRecall >= RECALL_THRESHOLD if (!passed) { console.error(`\n❌ Retrieval regression detected!`) - console.error(` Precision@${results.k}: ${results.meanPrecision.toFixed(3)} (threshold: ${PRECISION_THRESHOLD})`) + console.error( + ` Precision@${results.k}: ${results.meanPrecision.toFixed(3)} (threshold: ${PRECISION_THRESHOLD})` + ) console.error(` Recall@${results.k}: ${results.meanRecall.toFixed(3)} (threshold: ${RECALL_THRESHOLD})`) // Surface worst-performing queries - const worst = [...results.perQuery] - .sort((a, b) => a.precision - b.precision) - .slice(0, 3) + const worst = [...results.perQuery].sort((a, b) => a.precision - b.precision).slice(0, 3) console.error('\nWorst-performing queries:') - worst.forEach(q => console.error(` "${q.query}" → P=${q.precision.toFixed(2)}, R=${q.recall.toFixed(2)}`)) + worst.forEach((q) => + console.error(` "${q.query}" → P=${q.precision.toFixed(2)}, R=${q.recall.toFixed(2)}`) + ) process.exit(1) } -console.log(`\n✓ Retrieval quality OK (P@${results.k}=${results.meanPrecision.toFixed(3)}, R@${results.k}=${results.meanRecall.toFixed(3)})`) +console.log( + `\n✓ Retrieval quality OK (P@${results.k}=${results.meanPrecision.toFixed(3)}, R@${results.k}=${results.meanRecall.toFixed(3)})` +) process.exit(0) ``` @@ -494,7 +486,7 @@ name: RAG Evaluation on: pull_request: paths: - - 'src/**' # Adjust to your source paths + - 'src/**' # Adjust to your source paths jobs: eval: @@ -569,13 +561,13 @@ print('Results saved to eval_history.jsonl') ## Interpreting results -| Signal | Likely cause | Action | -|---|---|---| -| Low Precision@k (< 0.4) | Too many off-topic results | Reduce `limit`, tighten `where` filters, or increase score threshold | -| Low Recall@k (< 0.4) | Missing relevant chunks | Check index status (`ready`?), increase `limit`, review chunking strategy | -| Score drift across a PR | Embedding model update | Review BYOV or model config change; re-evaluate thresholds | -| Specific query clusters failing | Coverage gap in corpus | Ingest additional source material for those topics | -| Single label dominating results | Label imbalance | Balance corpus or search labels individually then merge | +| Signal | Likely cause | Action | +| ------------------------------- | -------------------------- | ------------------------------------------------------------------------- | +| Low Precision@k (< 0.4) | Too many off-topic results | Reduce `limit`, tighten `where` filters, or increase score threshold | +| Low Recall@k (< 0.4) | Missing relevant chunks | Check index status (`ready`?), increase `limit`, review chunking strategy | +| Score drift across a PR | Embedding model update | Review BYOV or model config change; re-evaluate thresholds | +| Specific query clusters failing | Coverage gap in corpus | Ingest additional source material for those topics | +| Single label dominating results | Label imbalance | Balance corpus or search labels individually then merge | --- @@ -590,6 +582,6 @@ print('Results saved to eval_history.jsonl') ## Next steps -- [RAG Reranking](./rag-reranking) — improve Precision@k by adding a second retrieval stage -- [Multi-Source RAG](./rag-multi-source) — extend evaluation to cross-label retrieval -- [GraphRAG](./graphrag) — measure enrichment quality alongside base retrieval metrics +- [RAG Reranking](/tutorials/rag-reranking) — improve Precision@k by adding a second retrieval stage +- [Multi-Source RAG](/tutorials/rag-multi-source) — extend evaluation to cross-label retrieval +- [GraphRAG](/tutorials/graphrag) — measure enrichment quality alongside base retrieval metrics diff --git a/docs/docs/tutorials/rag-multi-source.mdx b/docs/docs/learn/tutorials/ai-and-rag/rag-multi-source.mdx similarity index 95% rename from docs/docs/tutorials/rag-multi-source.mdx rename to docs/docs/learn/tutorials/ai-and-rag/rag-multi-source.mdx index 0a18912b..b0d1a269 100644 --- a/docs/docs/tutorials/rag-multi-source.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/rag-multi-source.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/rag-multi-source sidebar_position: 39 -title: "Multi-Source RAG" +title: 'Multi-Source RAG' description: Ingest PDFs, web pages, and database records as distinct labels, then search across all sources in a single vector query with source-aware citations. tags: [RAG, AI, Embeddings, Search] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Multi-Source RAG @@ -217,7 +218,7 @@ For structured data, generate a natural-language summary per entity and store it async function ingestDbSummaries(products) { await db.records.importJson({ label: 'DB_SUMMARY', - data: products.map(p => ({ + data: products.map((p) => ({ text: `${p.name}: ${p.description}. Category: ${p.category}. Price: $${p.price}. In stock: ${p.stock}.`, entityId: p.id, entityType: 'product', @@ -431,14 +432,14 @@ Drop low-confidence results and remove near-duplicate chunks before sending to t ```js -function deduplicateChunks(hits, scoreThreshold = 0.70, similarityThreshold = 0.95) { +function deduplicateChunks(hits, scoreThreshold = 0.7, similarityThreshold = 0.95) { // Drop low-score results - const filtered = hits.filter(h => (h.__score ?? 0) >= scoreThreshold) + const filtered = hits.filter((h) => (h.__score ?? 0) >= scoreThreshold) // Remove near-duplicates: skip a chunk if its text is too similar to one already kept const kept = [] for (const hit of filtered) { - const isDuplicate = kept.some(k => jaccardSimilarity(k.text, hit.text) >= similarityThreshold) + const isDuplicate = kept.some((k) => jaccardSimilarity(k.text, hit.text) >= similarityThreshold) if (!isDuplicate) kept.push(hit) } return kept @@ -448,7 +449,7 @@ function deduplicateChunks(hits, scoreThreshold = 0.70, similarityThreshold = 0. function jaccardSimilarity(a, b) { const setA = new Set(a.toLowerCase().split(/\s+/)) const setB = new Set(b.toLowerCase().split(/\s+/)) - const intersection = [...setA].filter(w => setB.has(w)).length + const intersection = [...setA].filter((w) => setB.has(w)).length const union = new Set([...setA, ...setB]).size return intersection / union } @@ -675,6 +676,6 @@ ingestDbSummaries()→ DB_SUMMARY records ## Next steps -- [RAG Evaluation](./rag-evaluation) — measure precision@k and recall@k across your pipeline -- [RAG Reranking](./rag-reranking) — two-stage retrieval with cross-encoder scoring -- [GraphRAG](./graphrag) — enrich chunks with graph context before synthesis +- [RAG Evaluation](/tutorials/rag-evaluation) — measure precision@k and recall@k across your pipeline +- [RAG Reranking](/tutorials/rag-reranking) — two-stage retrieval with cross-encoder scoring +- [GraphRAG](/tutorials/graphrag) — enrich chunks with graph context before synthesis diff --git a/docs/docs/tutorials/rag-pipeline.mdx b/docs/docs/learn/tutorials/ai-and-rag/rag-pipeline.mdx similarity index 97% rename from docs/docs/tutorials/rag-pipeline.mdx rename to docs/docs/learn/tutorials/ai-and-rag/rag-pipeline.mdx index 6c1e77df..2d39122a 100644 --- a/docs/docs/tutorials/rag-pipeline.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/rag-pipeline.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/rag-pipeline sidebar_position: 6 title: RAG Pipeline in Minutes description: Chunk Markdown files, store them in RushDB, and build a retrieval-augmented generation pipeline in TypeScript, Python, or REST. tags: [AI, RAG] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # RAG Pipeline in Minutes @@ -418,5 +419,5 @@ The `where` prefilter runs on the graph layer before semantic scoring — so you - Add more metadata fields (author, date, section heading) — they're queryable without any schema changes - Use `$startsWith` or `$in` on `source` to search across a subset of files -- Combine with [transactions](../typescript-sdk/transactions) to atomically re-ingest a file when it changes +- Combine with [transactions](/build/reliability/transactions) to atomically re-ingest a file when it changes - Replace the fixed-size chunker with a semantic splitter (split on headings, paragraphs, or sentences) diff --git a/docs/docs/tutorials/rag-reranking.mdx b/docs/docs/learn/tutorials/ai-and-rag/rag-reranking.mdx similarity index 86% rename from docs/docs/tutorials/rag-reranking.mdx rename to docs/docs/learn/tutorials/ai-and-rag/rag-reranking.mdx index 35cf9505..03314a74 100644 --- a/docs/docs/tutorials/rag-reranking.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/rag-reranking.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/rag-reranking sidebar_position: 41 -title: "RAG Reranking" +title: 'RAG Reranking' description: Improve retrieval precision with two-stage search — over-fetch candidates with vector similarity, then rerank with LLM scoring or Reciprocal Rank Fusion before sending to the LLM. tags: [RAG, AI, Embeddings, Search] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # RAG Reranking @@ -84,9 +85,7 @@ Relevance score:` }) ) - return scored - .sort((a, b) => b.rerankScore - a.rerankScore) - .slice(0, topK) + return scored.sort((a, b) => b.rerankScore - a.rerankScore).slice(0, topK) } // Combined pipeline @@ -97,7 +96,7 @@ async function retrieveAndRerank(query, topK = 5) { } const topChunks = await retrieveAndRerank('how does billing work for BYOC projects?') -topChunks.forEach(c => console.log(`[${c.rerankScore}/10] ${c.text.slice(0, 80)}...`)) +topChunks.forEach((c) => console.log(`[${c.rerankScore}/10] ${c.text.slice(0, 80)}...`)) ``` @@ -197,14 +196,12 @@ function rrfMerge(rankedLists, k = 60) { for (const list of rankedLists) { list.forEach((id, index) => { - const rank = index + 1 // 1-based + const rank = index + 1 // 1-based scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank)) }) } - return [...scores.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([id]) => id) + return [...scores.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id) } // Example: combine semantic search on body text with title-focused search @@ -224,16 +221,14 @@ async function hybridSearch(query, topK = 5) { }) ]) - const bodyIds = bodyHits.map(h => h.__id) - const titleIds = titleHits.map(h => h.__id) + const bodyIds = bodyHits.map((h) => h.__id) + const titleIds = titleHits.map((h) => h.__id) const mergedIds = rrfMerge([bodyIds, titleIds]).slice(0, topK) // Hydrate with original records (preserve all fields) - const recordMap = Object.fromEntries( - [...bodyHits, ...titleHits].map(h => [h.__id, h]) - ) - return mergedIds.map(id => recordMap[id]).filter(Boolean) + const recordMap = Object.fromEntries([...bodyHits, ...titleHits].map((h) => [h.__id, h])) + return mergedIds.map((id) => recordMap[id]).filter(Boolean) } export { rrfMerge, hybridSearch } @@ -308,14 +303,14 @@ async function precisionPipeline(query, finalTopK = 5) { }) // Stage 2: RRF merge - const semanticIds = semanticHits.map(h => h.__id) - const keywordIds = (keywordHits.data ?? []).map(h => h.__id) - const mergedIds = rrfMerge([semanticIds, keywordIds]).slice(0, 25) + const semanticIds = semanticHits.map((h) => h.__id) + const keywordIds = (keywordHits.data ?? []).map((h) => h.__id) + const mergedIds = rrfMerge([semanticIds, keywordIds]).slice(0, 25) // Hydrate merged const allRecords = [...semanticHits, ...(keywordHits.data ?? [])] - const recordMap = Object.fromEntries(allRecords.map(r => [r.__id, r])) - const merged = mergedIds.map(id => recordMap[id]).filter(Boolean) + const recordMap = Object.fromEntries(allRecords.map((r) => [r.__id, r])) + const merged = mergedIds.map((id) => recordMap[id]).filter(Boolean) // Stage 3: LLM rerank return rerankWithLlm(query, merged, finalTopK) @@ -361,15 +356,16 @@ def precision_pipeline(db, openai_client, query: str, final_top_k: int = 5) -> l ## Cost and latency trade-offs -| Strategy | Latency | Cost | Precision gain | -|---|---|---|---| -| Vector only (baseline) | ~50–200 ms | $ | — | -| RRF (multi-index) | ~100–400 ms | $ | Low–Medium | -| LLM rerank (gpt-4o-mini) | +500–2000 ms | $$ | Medium–High | -| LLM rerank (gpt-4o) | +1000–4000 ms | $$$ | High | -| RRF + LLM rerank | +600–2500 ms | $$ | High | +| Strategy | Latency | Cost | Precision gain | +| ------------------------ | ------------- | ---- | -------------- | +| Vector only (baseline) | ~50–200 ms | $ | — | +| RRF (multi-index) | ~100–400 ms | $ | Low–Medium | +| LLM rerank (gpt-4o-mini) | +500–2000 ms | $$ | Medium–High | +| LLM rerank (gpt-4o) | +1000–4000 ms | $$$ | High | +| RRF + LLM rerank | +600–2500 ms | $$ | High | **Practical guidance:** + - Use **vector only** for real-time type-ahead or high-volume search where P@5 > 0.6 is already achieved. - Use **RRF** when you have multiple meaningful retrieval signals (titles, bodies, semantic, exact match) and want free precision gains. - Use **LLM rerank** when you need the highest possible precision for low-traffic, high-stakes queries (support tickets, legal research, medical Q&A). @@ -389,7 +385,13 @@ const rerankCache = new Map() async function cachedRerank(query, candidates, topK = 5) { // Cache key: query + sorted candidate IDs - const cacheKey = query + '|' + candidates.map(c => c.__id).sort().join(',') + const cacheKey = + query + + '|' + + candidates + .map((c) => c.__id) + .sort() + .join(',') if (rerankCache.has(cacheKey)) { return rerankCache.get(cacheKey) @@ -454,6 +456,6 @@ LLM.chat(prompt) ← Final answer with citations ## Next steps -- [RAG Evaluation](./rag-evaluation) — measure the Precision@k impact of adding reranking -- [Multi-Source RAG](./rag-multi-source) — apply RRF across PDF, web, and database labels -- [GraphRAG](./graphrag) — add graph-enriched context alongside reranked chunks +- [RAG Evaluation](/tutorials/rag-evaluation) — measure the Precision@k impact of adding reranking +- [Multi-Source RAG](/tutorials/rag-multi-source) — apply RRF across PDF, web, and database labels +- [GraphRAG](/tutorials/graphrag) — add graph-enriched context alongside reranked chunks diff --git a/docs/docs/tutorials/semantic-search-multitenant.mdx b/docs/docs/learn/tutorials/ai-and-rag/semantic-search-multitenant.mdx similarity index 95% rename from docs/docs/tutorials/semantic-search-multitenant.mdx rename to docs/docs/learn/tutorials/ai-and-rag/semantic-search-multitenant.mdx index 3f6bc549..2800c848 100644 --- a/docs/docs/tutorials/semantic-search-multitenant.mdx +++ b/docs/docs/learn/tutorials/ai-and-rag/semantic-search-multitenant.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/semantic-search-multitenant sidebar_position: 14 -title: "Semantic Search for Multi-Tenant Products" +title: 'Semantic Search for Multi-Tenant Products' description: Build tenant-safe semantic retrieval using RushDB's project-scoped prefilter and exact cosine similarity ranking — without global index assumptions. tags: [AI, Search, Multi-Tenant, Architecture] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Semantic Search for Multi-Tenant Products @@ -37,6 +38,7 @@ graph LR The prefilter step narrows candidates to records in the current project (via a Cypher MATCH/WHERE clause) before any similarity computation runs. Exact cosine similarity is then applied to the prefiltered set. This means: + - Tenant isolation is guaranteed by the query engine, not by application-layer filtering - Adding a `where` clause narrows the candidate set further but does not change the isolation guarantee - Results always carry a `__score` (0–1) indicating cosine similarity @@ -156,7 +158,7 @@ Semantic search requires an embedding index on the property you want to search. const index = await db.ai.indexes.create({ label: 'ARTICLE', propertyName: 'body', - sourceType: 'managed' // RushDB embeds automatically + sourceType: 'managed' // RushDB embeds automatically }) console.log('Index status:', index.data.status) @@ -204,7 +206,7 @@ async function waitForIndex(indexId: string, intervalMs = 3000): Promise { const { totalRecords, indexedRecords } = stats.data console.log(`Indexed ${indexedRecords} / ${totalRecords}`) if (indexedRecords >= totalRecords) break - await new Promise(r => setTimeout(r, intervalMs)) + await new Promise((r) => setTimeout(r, intervalMs)) } } @@ -496,19 +498,23 @@ await db.records.create({ data: { title: 'Distributed Tracing at Scale', body: 'How to instrument services for end-to-end trace collection.', - category: 'observability', + category: 'observability' }, vectors: [ { propertyName: 'body', - vector: [/* 1536-dimension float array from your embedding model */] + vector: [ + /* 1536-dimension float array from your embedding model */ + ] } ] }) // Search with an external query vector const vectorResults = await db.ai.search({ - queryVector: [/* same 1536-dimension array */], + queryVector: [ + /* same 1536-dimension array */ + ], propertyName: 'body', labels: ['ARTICLE'], sourceType: 'external', @@ -607,7 +613,7 @@ export async function searchHandler(req: Request): Promise { }) return Response.json({ - results: results.data.map(r => ({ + results: results.data.map((r) => ({ id: r.__id, title: r.title, score: r.__score, @@ -686,6 +692,6 @@ Project-scoped isolation is enforced at the API key level. If you accidentally r ## Next steps -- [Hybrid Retrieval: Structured Filters Plus Semantic Search](./hybrid-retrieval.mdx) — combining semantic ranking with deeper graph traversal -- [RAG Pipeline in Minutes](./rag-pipeline.mdx) — adding an LLM generation step on top of retrieval -- [Semantic Search reference](../typescript-sdk/ai/search.md) — full parameter reference +- [Hybrid Retrieval: Structured Filters Plus Semantic Search](/tutorials/hybrid-retrieval) — combining semantic ranking with deeper graph traversal +- [RAG Pipeline in Minutes](/tutorials/rag-pipeline) — adding an LLM generation step on top of retrieval +- [Semantic Search reference](/build/ai-search/semantic-search) — full parameter reference diff --git a/docs/docs/tutorials/choosing-relationship-types.mdx b/docs/docs/learn/tutorials/graph-modeling/choosing-relationship-types.mdx similarity index 93% rename from docs/docs/tutorials/choosing-relationship-types.mdx rename to docs/docs/learn/tutorials/graph-modeling/choosing-relationship-types.mdx index e113a4d2..dbb2f24e 100644 --- a/docs/docs/tutorials/choosing-relationship-types.mdx +++ b/docs/docs/learn/tutorials/graph-modeling/choosing-relationship-types.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/choosing-relationship-types sidebar_position: 15 -title: "Choosing Relationship Types That Age Well" +title: 'Choosing Relationship Types That Age Well' description: When to use generic nesting-driven edges versus explicit typed relationships, and how that choice affects readability, search, and analytics downstream. tags: [Graph Modeling, Concepts, Relationships] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Choosing Relationship Types That Age Well @@ -28,10 +29,12 @@ You let RushDB's automatic nesting create edges by importing JSON with deeply ne ```typescript await db.records.importJson({ label: 'ORDER', - data: [{ - total: 149.00, - PRODUCT: [{ name: 'Lens Cap 58mm', price: 12.99 }] - }] + data: [ + { + total: 149.0, + PRODUCT: [{ name: 'Lens Cap 58mm', price: 12.99 }] + } + ] }) ``` @@ -76,7 +79,7 @@ You create records independently and attach them with a named relationship type: ```typescript -const order = await db.records.create({ label: 'ORDER', data: { total: 149.00 } }) +const order = await db.records.create({ label: 'ORDER', data: { total: 149.0 } }) const product = await db.records.create({ label: 'PRODUCT', data: { name: 'Lens Cap 58mm', price: 12.99 } }) await db.records.attach({ @@ -124,7 +127,7 @@ curl -s -X POST "$BASE/records/$ORDER_ID/relations" \ -The edge now carries meaning: an `ORDER` *contains* a `PRODUCT`. +The edge now carries meaning: an `ORDER` _contains_ a `PRODUCT`. --- @@ -201,7 +204,7 @@ curl -s -X POST "$BASE/records/search" \ -This query returns documents connected to Lena — but connected *how*? Did she write them? Read them? Approve them? The graph cannot answer that question. +This query returns documents connected to Lena — but connected _how_? Did she write them? Read them? Approve them? The graph cannot answer that question. With typed relationships, the intent is explicit: @@ -463,6 +466,7 @@ Relationship types that age well share a few properties: - **Uppercase**: consistent with Neo4j conventions and easier to scan in query code Avoid: + - `HAS` — does not indicate what the relationship means - `LINKED` — directionally ambiguous - `REL_USER_DOCUMENT` — table-join style naming @@ -477,6 +481,6 @@ You cannot rename or change a relationship type after records are attached using ## Next steps -- [Modeling Hierarchies, Networks, and Feedback Loops](./modeling-hierarchies.mdx) — three common graph shapes and how to query each -- [Thinking in Graphs: From Tables to Traversals](./thinking-in-graphs.mdx) — the full mental model shift -- [SearchQuery Deep Dive](./searchquery-advanced-patterns.mdx) — `$relation`, `$alias`, and aggregation patterns +- [Modeling Hierarchies, Networks, and Feedback Loops](/tutorials/modeling-hierarchies) — three common graph shapes and how to query each +- [Thinking in Graphs: From Tables to Traversals](/tutorials/thinking-in-graphs) — the full mental model shift +- [SearchQuery Deep Dive](/tutorials/searchquery-advanced-patterns) — `$relation`, `$alias`, and aggregation patterns diff --git a/docs/docs/tutorials/customer-360.mdx b/docs/docs/learn/tutorials/graph-modeling/customer-360.mdx similarity index 91% rename from docs/docs/tutorials/customer-360.mdx rename to docs/docs/learn/tutorials/graph-modeling/customer-360.mdx index ad6cd107..100b472c 100644 --- a/docs/docs/tutorials/customer-360.mdx +++ b/docs/docs/learn/tutorials/graph-modeling/customer-360.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/customer-360 sidebar_position: 26 -title: "Customer 360 as a Connected Graph" +title: 'Customer 360 as a Connected Graph' description: Model users, accounts, subscriptions, invoices, touchpoints, and support interactions as a connected graph so customer context becomes retrievable instead of siloed. tags: [Domain Blueprint, Relationships, SearchQuery, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Customer 360 as a Connected Graph @@ -28,15 +29,15 @@ graph LR ACCOUNT -->|USES_FEATURE| FEATURE_USAGE[FEATURE_USAGE] ``` -| Label | What it represents | -|---|---| -| `USER` | An individual user with login/profile data | -| `ACCOUNT` | An organization or company | -| `SUBSCRIPTION` | An active or cancelled plan | -| `INVOICE` | A billing invoice | -| `TOUCHPOINT` | A marketing or sales interaction (email, call, demo) | -| `SUPPORT_TICKET` | An inbound support request | -| `FEATURE_USAGE` | A log of which product features are used and how often | +| Label | What it represents | +| ---------------- | ------------------------------------------------------ | +| `USER` | An individual user with login/profile data | +| `ACCOUNT` | An organization or company | +| `SUBSCRIPTION` | An active or cancelled plan | +| `INVOICE` | A billing invoice | +| `TOUCHPOINT` | A marketing or sales interaction (email, call, demo) | +| `SUPPORT_TICKET` | An inbound support request | +| `FEATURE_USAGE` | A log of which product features are used and how often | --- @@ -67,7 +68,7 @@ await db.records.importJson({ label: 'USER', data: [ { email: 'alice@acme.com', name: 'Alice', role: 'admin', lastActiveAt: '2025-03-01' }, - { email: 'bob@acme.com', name: 'Bob', role: 'member', lastActiveAt: '2025-02-20' } + { email: 'bob@acme.com', name: 'Bob', role: 'member', lastActiveAt: '2025-02-20' } ] }) @@ -479,6 +480,6 @@ Customer graphs grow with every activity event. Feature usage logs and touchpoin ## Next steps -- [Incident Response Graphs](./incident-response) — add operational context to account graphs -- [Hybrid Retrieval](./hybrid-retrieval.mdx) — semantic search within a specific account's records -- [Building a Graph-Backed API Layer](./graph-backed-api.mdx) — expose this graph through a production API +- [Incident Response Graphs](/tutorials/incident-response) — add operational context to account graphs +- [Hybrid Retrieval](/tutorials/hybrid-retrieval) — semantic search within a specific account's records +- [Building a Graph-Backed API Layer](/tutorials/graph-backed-api) — expose this graph through a production API diff --git a/docs/docs/tutorials/data-lineage.mdx b/docs/docs/learn/tutorials/graph-modeling/data-lineage.mdx similarity index 95% rename from docs/docs/tutorials/data-lineage.mdx rename to docs/docs/learn/tutorials/graph-modeling/data-lineage.mdx index 7a8cbb76..0f7ccea7 100644 --- a/docs/docs/tutorials/data-lineage.mdx +++ b/docs/docs/learn/tutorials/graph-modeling/data-lineage.mdx @@ -1,16 +1,17 @@ --- +slug: /tutorials/data-lineage sidebar_position: 12 -title: "End-to-End Data Lineage: From Source to Answer" +title: 'End-to-End Data Lineage: From Source to Answer' description: Model imported records, transformation steps, derived summaries, and final outputs so every answer can be traced back to its upstream source. tags: [Lineage, Audit, Graph Modeling, Governance] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # End-to-End Data Lineage: From Source to Answer -When a dashboard shows a number, a sales rep acts on it. When a model generates a recommendation, an engineer ships it. Lineage answers the question that always follows: *where did this come from?* +When a dashboard shows a number, a sales rep acts on it. When a model generates a recommendation, an engineer ships it. Lineage answers the question that always follows: _where did this come from?_ This tutorial models a data pipeline as a graph: @@ -147,7 +148,7 @@ const enrichRun = await db.records.create({ // Link sources into this run await Promise.all([ db.records.attach({ source: crmExport, target: enrichRun, options: { type: 'FEEDS' } }), - db.records.attach({ source: billingSnapshot, target: enrichRun, options: { type: 'FEEDS' } }), + db.records.attach({ source: billingSnapshot, target: enrichRun, options: { type: 'FEEDS' } }) ]) ``` @@ -285,7 +286,7 @@ const summaryArtifact = await db.records.create({ await Promise.all([ db.records.attach({ source: enrichedArtifact, target: summaryRun, options: { type: 'FEEDS' } }), - db.records.attach({ source: summaryRun, target: summaryArtifact, options: { type: 'PRODUCED' } }), + db.records.attach({ source: summaryRun, target: summaryArtifact, options: { type: 'PRODUCED' } }) ]) ``` @@ -596,6 +597,6 @@ Multi-hop lineage queries will scan the entire reachable subgraph unless you sco ## Next steps -- [Audit Trails with Immutable Events](./audit-trails.mdx) — separate event log from current state for reconstructible history -- [Versioning Records Without Losing Queryability](./versioning-records) — keeping historical state queryable alongside current state -- [RushDB as a Memory Layer](./memory-layer.mdx) — using the same EPISODE + REFERENCE pattern for agent memory +- [Audit Trails with Immutable Events](/tutorials/audit-trails) — separate event log from current state for reconstructible history +- [Versioning Records Without Losing Queryability](/tutorials/versioning-records) — keeping historical state queryable alongside current state +- [RushDB as a Memory Layer](/tutorials/memory-layer) — using the same EPISODE + REFERENCE pattern for agent memory diff --git a/docs/docs/tutorials/modeling-hierarchies.mdx b/docs/docs/learn/tutorials/graph-modeling/modeling-hierarchies.mdx similarity index 90% rename from docs/docs/tutorials/modeling-hierarchies.mdx rename to docs/docs/learn/tutorials/graph-modeling/modeling-hierarchies.mdx index 0f5c8025..a0f0fd96 100644 --- a/docs/docs/tutorials/modeling-hierarchies.mdx +++ b/docs/docs/learn/tutorials/graph-modeling/modeling-hierarchies.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/modeling-hierarchies sidebar_position: 16 -title: "Modeling Hierarchies, Networks, and Feedback Loops" +title: 'Modeling Hierarchies, Networks, and Feedback Loops' description: Three common graph shapes — trees, many-to-many networks, and cyclic systems — with guidance on how to query each without flattening away meaning. tags: [Graph Modeling, Concepts, SearchQuery, Traversal] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Modeling Hierarchies, Networks, and Feedback Loops @@ -43,8 +44,14 @@ const company = await db.records.create({ label: 'COMPANY', data: { name: 'Acme const division = await db.records.create({ label: 'DIVISION', data: { name: 'Engineering' } }) const dept = await db.records.create({ label: 'DEPARTMENT', data: { name: 'Platform', budget: 2000000 } }) const teamInfra = await db.records.create({ label: 'TEAM', data: { name: 'Infra', size: 6 } }) -const lena = await db.records.create({ label: 'EMPLOYEE', data: { name: 'Lena Müller', role: 'Lead SRE', level: 'L5' } }) -const marco = await db.records.create({ label: 'EMPLOYEE', data: { name: 'Marco Rossi', role: 'Engineer', level: 'L4' } }) +const lena = await db.records.create({ + label: 'EMPLOYEE', + data: { name: 'Lena Müller', role: 'Lead SRE', level: 'L5' } +}) +const marco = await db.records.create({ + label: 'EMPLOYEE', + data: { name: 'Marco Rossi', role: 'Engineer', level: 'L4' } +}) await db.records.attach({ source: company, target: division, options: { type: 'HAS_DIVISION' } }) await db.records.attach({ source: division, target: dept, options: { type: 'HAS_DEPARTMENT' } }) @@ -271,15 +278,21 @@ const lenaAuthor = await db.records.create({ label: 'AUTHOR', data: { name: 'Len const marcoAuthor = await db.records.create({ label: 'AUTHOR', data: { name: 'Marco Rossi', hIndex: 9 } }) const topicGNN = await db.records.create({ label: 'TOPIC', data: { name: 'Graph Neural Networks' } }) const topicDistrib = await db.records.create({ label: 'TOPIC', data: { name: 'Distributed Systems' } }) -const paper1 = await db.records.create({ label: 'PAPER', data: { title: 'GNN Scaling Strategies', year: 2024, citations: 87 } }) -const paper2 = await db.records.create({ label: 'PAPER', data: { title: 'Attention is All You Need', year: 2017, citations: 90000 } }) +const paper1 = await db.records.create({ + label: 'PAPER', + data: { title: 'GNN Scaling Strategies', year: 2024, citations: 87 } +}) +const paper2 = await db.records.create({ + label: 'PAPER', + data: { title: 'Attention is All You Need', year: 2017, citations: 90000 } +}) await Promise.all([ db.records.attach({ source: lenaAuthor, target: paper1, options: { type: 'CO_AUTHORED' } }), db.records.attach({ source: marcoAuthor, target: paper1, options: { type: 'CO_AUTHORED' } }), db.records.attach({ source: paper1, target: topicGNN, options: { type: 'COVERS' } }), db.records.attach({ source: paper1, target: topicDistrib, options: { type: 'COVERS' } }), - db.records.attach({ source: paper1, target: paper2, options: { type: 'CITES' } }), + db.records.attach({ source: paper1, target: paper2, options: { type: 'CITES' } }) ]) ``` @@ -414,14 +427,20 @@ SearchQuery does not expose arbitrary-depth recursive traversal. Instead, scope ```typescript const appCore = await db.records.create({ label: 'PACKAGE', data: { name: 'app-core', version: '2.1.0' } }) const authLib = await db.records.create({ label: 'PACKAGE', data: { name: 'auth-lib', version: '1.4.2' } }) -const dataClient = await db.records.create({ label: 'PACKAGE', data: { name: 'data-client', version: '3.0.1' } }) -const cryptoUtils = await db.records.create({ label: 'PACKAGE', data: { name: 'crypto-utils', version: '0.9.8' } }) +const dataClient = await db.records.create({ + label: 'PACKAGE', + data: { name: 'data-client', version: '3.0.1' } +}) +const cryptoUtils = await db.records.create({ + label: 'PACKAGE', + data: { name: 'crypto-utils', version: '0.9.8' } +}) await Promise.all([ db.records.attach({ source: appCore, target: authLib, options: { type: 'DEPENDS_ON' } }), db.records.attach({ source: appCore, target: dataClient, options: { type: 'DEPENDS_ON' } }), db.records.attach({ source: authLib, target: cryptoUtils, options: { type: 'DEPENDS_ON' } }), - db.records.attach({ source: dataClient, target: cryptoUtils, options: { type: 'DEPENDS_ON' } }), + db.records.attach({ source: dataClient, target: cryptoUtils, options: { type: 'DEPENDS_ON' } }) ]) ``` @@ -573,11 +592,11 @@ curl -s -X POST "$BASE/records/search" \ ## Comparison of the three shapes -| Shape | Key property | Query pattern | Ambush | -|---|---|---|---| -| Tree | Single parent per node | Top-down with full-path select | Deep trees require explicit hop count | -| Many-to-many | Nodes can appear in multiple relationships | Aggregation by relationship type | Fan-out can be large without limit | -| Cyclic | Loops are possible | Explicit depth bounds | Unbounded traversal is not supported | +| Shape | Key property | Query pattern | Ambush | +| ------------ | ------------------------------------------ | -------------------------------- | ------------------------------------- | +| Tree | Single parent per node | Top-down with full-path select | Deep trees require explicit hop count | +| Many-to-many | Nodes can appear in multiple relationships | Aggregation by relationship type | Fan-out can be large without limit | +| Cyclic | Loops are possible | Explicit depth bounds | Unbounded traversal is not supported | --- @@ -591,6 +610,6 @@ Apply `limit` conservatively and filter early on high-selectivity properties (e. ## Next steps -- [Choosing Relationship Types That Age Well](./choosing-relationship-types.mdx) — when generic vs. typed edges matter -- [Temporal Graphs: Modeling State and Event Time Together](./temporal-graphs.mdx) — adding time dimension to any of these shapes -- [SearchQuery Deep Dive](./searchquery-advanced-patterns.mdx) — `$relation`, `$alias`, and `collect` patterns +- [Choosing Relationship Types That Age Well](/tutorials/choosing-relationship-types) — when generic vs. typed edges matter +- [Temporal Graphs: Modeling State and Event Time Together](/tutorials/temporal-graphs) — adding time dimension to any of these shapes +- [SearchQuery Deep Dive](/tutorials/searchquery-advanced-patterns) — `$relation`, `$alias`, and `collect` patterns diff --git a/docs/docs/tutorials/temporal-graphs.mdx b/docs/docs/learn/tutorials/graph-modeling/temporal-graphs.mdx similarity index 97% rename from docs/docs/tutorials/temporal-graphs.mdx rename to docs/docs/learn/tutorials/graph-modeling/temporal-graphs.mdx index c9c0e22c..1ba4d577 100644 --- a/docs/docs/tutorials/temporal-graphs.mdx +++ b/docs/docs/learn/tutorials/graph-modeling/temporal-graphs.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/temporal-graphs sidebar_position: 17 title: 'Temporal Graphs: Modeling State and Event Time Together' description: Represent durable entities alongside time-stamped events so you can answer both current-state and historical questions without losing lineage. @@ -550,6 +551,6 @@ State and event chains grow indefinitely. Set a retention horizon: archive or de ## Next steps -- [Audit Trails with Immutable Events and Derived State](./audit-trails.mdx) — extending this pattern for compliance use cases -- [RushDB as a Memory Layer](./memory-layer.mdx) — using the same FACT/EPISODE pattern for agent memory -- [SearchQuery Deep Dive](./searchquery-advanced-patterns.mdx) — date filter syntax and aggregation patterns +- [Audit Trails with Immutable Events and Derived State](/tutorials/audit-trails) — extending this pattern for compliance use cases +- [RushDB as a Memory Layer](/tutorials/memory-layer) — using the same FACT/EPISODE pattern for agent memory +- [SearchQuery Deep Dive](/tutorials/searchquery-advanced-patterns) — date filter syntax and aggregation patterns diff --git a/docs/docs/tutorials/thinking-in-graphs.mdx b/docs/docs/learn/tutorials/graph-modeling/thinking-in-graphs.mdx similarity index 89% rename from docs/docs/tutorials/thinking-in-graphs.mdx rename to docs/docs/learn/tutorials/graph-modeling/thinking-in-graphs.mdx index e24ed0b2..e74d94a3 100644 --- a/docs/docs/tutorials/thinking-in-graphs.mdx +++ b/docs/docs/learn/tutorials/graph-modeling/thinking-in-graphs.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/thinking-in-graphs sidebar_position: 10 -title: "Thinking in Graphs: From Tables to Traversals" +title: 'Thinking in Graphs: From Tables to Traversals' description: Map the same product, customer, and order dataset from relational and document mental models into RushDB's graph model, then translate common business questions into multi-hop queries. tags: [Concepts, Graph Modeling, SearchQuery] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Thinking in Graphs: From Tables to Traversals @@ -58,14 +59,10 @@ In SQL you answer "which customers reviewed a product they never ordered?" with { "orderId": "o1", "status": "shipped", - "items": [ - { "productId": "p1", "qty": 2 } - ] + "items": [{ "productId": "p1", "qty": 2 }] } ], - "reviews": [ - { "productId": "p2", "rating": 5 } - ] + "reviews": [{ "productId": "p2", "rating": 5 }] } ``` @@ -102,35 +99,41 @@ const db = new RushDB('RUSHDB_API_KEY') // Categories const [photography, audio] = await Promise.all([ db.records.create({ label: 'CATEGORY', data: { name: 'Photography' } }), - db.records.create({ label: 'CATEGORY', data: { name: 'Audio' } }), + db.records.create({ label: 'CATEGORY', data: { name: 'Audio' } }) ]) // Products const [lensCap, cameraBag, headphones] = await Promise.all([ db.records.create({ label: 'PRODUCT', data: { name: 'Lens Cap 58mm', price: 12.99 } }), - db.records.create({ label: 'PRODUCT', data: { name: 'Camera Bag Pro', price: 89.00 } }), - db.records.create({ label: 'PRODUCT', data: { name: 'Studio Headphones', price: 149.00 } }), + db.records.create({ label: 'PRODUCT', data: { name: 'Camera Bag Pro', price: 89.0 } }), + db.records.create({ label: 'PRODUCT', data: { name: 'Studio Headphones', price: 149.0 } }) ]) // Link products to categories await Promise.all([ db.records.attach({ source: lensCap, target: photography, options: { type: 'IN_CATEGORY' } }), db.records.attach({ source: cameraBag, target: photography, options: { type: 'IN_CATEGORY' } }), - db.records.attach({ source: headphones, target: audio, options: { type: 'IN_CATEGORY' } }), + db.records.attach({ source: headphones, target: audio, options: { type: 'IN_CATEGORY' } }) ]) // Customers const [lena, marco] = await Promise.all([ db.records.create({ label: 'CUSTOMER', data: { name: 'Lena Müller', email: 'lena@example.com' } }), - db.records.create({ label: 'CUSTOMER', data: { name: 'Marco Rossi', email: 'marco@example.com' } }), + db.records.create({ label: 'CUSTOMER', data: { name: 'Marco Rossi', email: 'marco@example.com' } }) ]) // Orders -const order1 = await db.records.create({ label: 'ORDER', data: { status: 'shipped', placedAt: '2025-01-10' } }) +const order1 = await db.records.create({ + label: 'ORDER', + data: { status: 'shipped', placedAt: '2025-01-10' } +}) await db.records.attach({ source: lena, target: order1, options: { type: 'PLACED' } }) await db.records.attach({ source: order1, target: lensCap, options: { type: 'CONTAINS' } }) -const order2 = await db.records.create({ label: 'ORDER', data: { status: 'delivered', placedAt: '2025-02-14' } }) +const order2 = await db.records.create({ + label: 'ORDER', + data: { status: 'delivered', placedAt: '2025-02-14' } +}) await db.records.attach({ source: marco, target: order2, options: { type: 'PLACED' } }) await db.records.attach({ source: order2, target: cameraBag, options: { type: 'CONTAINS' } }) @@ -595,23 +598,23 @@ curl -s -X POST "$BASE/records/search" \ ## What changed between the mental models -| Concern | Relational | Document | Graph | -|---|---|---|---| -| Multi-hop traversal | Multi-join SQL | Iteration across documents | Single query with nested `where` | +| Concern | Relational | Document | Graph | +| ------------------------------ | ------------------------------------ | -------------------------------- | -------------------------------------- | +| Multi-hop traversal | Multi-join SQL | Iteration across documents | Single query with nested `where` | | Adding a new relationship type | New foreign key column or join table | Schema migration or array append | New `attach` call, zero schema changes | -| Querying along a new path | Rewrite query or add index | Rewrite aggregation logic | Extend existing `where` block | -| Metrics along path | GROUP BY with joins | Map-reduce | Per-hop metrics in same query | +| Querying along a new path | Rewrite query or add index | Rewrite aggregation logic | Extend existing `where` block | +| Metrics along path | GROUP BY with joins | Map-reduce | Per-hop metrics in same query | --- ## Production caveat -Relationship traversal queries become expensive when each hop fans out to thousands of related records. Before deploying traversal-heavy queries in production, scope them aggressively with `limit` on the leaf label and property filters that eliminate most candidates early. The [`SearchQuery Deep Dive`](./searchquery-advanced-patterns.mdx) tutorial covers aggregation and traversal optimization in more detail. +Relationship traversal queries become expensive when each hop fans out to thousands of related records. Before deploying traversal-heavy queries in production, scope them aggressively with `limit` on the leaf label and property filters that eliminate most candidates early. The [`SearchQuery Deep Dive`](/tutorials/searchquery-advanced-patterns) tutorial covers aggregation and traversal optimization in more detail. --- ## Next steps -- [Choosing Relationship Types That Age Well](./choosing-relationship-types.mdx) — when to use generic edges versus typed relationships -- [SearchQuery Deep Dive](./searchquery-advanced-patterns.mdx) — aggregation, collect, and groupBy patterns -- [RushDB as a Memory Layer](./memory-layer.mdx) — using the same graph primitives for agent memory +- [Choosing Relationship Types That Age Well](/tutorials/choosing-relationship-types) — when to use generic edges versus typed relationships +- [SearchQuery Deep Dive](/tutorials/searchquery-advanced-patterns) — aggregation, collect, and groupBy patterns +- [RushDB as a Memory Layer](/tutorials/memory-layer) — using the same graph primitives for agent memory diff --git a/docs/docs/tutorials/versioning-records.mdx b/docs/docs/learn/tutorials/graph-modeling/versioning-records.mdx similarity index 96% rename from docs/docs/tutorials/versioning-records.mdx rename to docs/docs/learn/tutorials/graph-modeling/versioning-records.mdx index 87995b31..16dec177 100644 --- a/docs/docs/tutorials/versioning-records.mdx +++ b/docs/docs/learn/tutorials/graph-modeling/versioning-records.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/versioning-records sidebar_position: 25 title: 'Versioning Records Without Losing Queryability' description: Compare in-place mutation, append-only versions, and hybrid versioning approaches — and how to query latest state while preserving historical analysis. @@ -345,7 +346,7 @@ curl -s -X POST "$BASE/records/search" \ ## Approach 3: Hybrid (mutable state + immutable events) -Keep the entity's current state queryable in one record while logging all changes as immutable EVENT records. This is the pattern from [Audit Trails](./audit-trails.mdx) and [Temporal Graphs](./temporal-graphs.mdx). Choose this when: +Keep the entity's current state queryable in one record while logging all changes as immutable EVENT records. This is the pattern from [Audit Trails](/tutorials/audit-trails) and [Temporal Graphs](/tutorials/temporal-graphs). Choose this when: - You need efficient current-state queries (no traversal to find latest version) - You need a history log (who changed what, when) @@ -373,6 +374,6 @@ Append-only VERSION chains grow linearly with edit frequency. If documents are e ## Next steps -- [Audit Trails](./audit-trails.mdx) — immutable event log alongside mutable state -- [Temporal Graphs](./temporal-graphs.mdx) — point-in-time reconstruction -- [Compliance and Retention Patterns](./compliance-retention) — archival and deletion +- [Audit Trails](/tutorials/audit-trails) — immutable event log alongside mutable state +- [Temporal Graphs](/tutorials/temporal-graphs) — point-in-time reconstruction +- [Compliance and Retention Patterns](/tutorials/compliance-retention) — archival and deletion diff --git a/docs/docs/tutorials/index.mdx b/docs/docs/learn/tutorials/index.mdx similarity index 95% rename from docs/docs/tutorials/index.mdx rename to docs/docs/learn/tutorials/index.mdx index 877b360d..3e3aba97 100644 --- a/docs/docs/tutorials/index.mdx +++ b/docs/docs/learn/tutorials/index.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/ title: Tutorials description: Step-by-step guides to build real things with RushDB — from deployment to AI-powered search and RAG pipelines. sidebar_position: 0 @@ -10,6 +11,6 @@ pagination_next: null pagination_prev: null --- -import TutorialsIndex from '@site/src/components/TutorialsIndex'; +import TutorialsIndex from '@site/src/components/TutorialsIndex' diff --git a/docs/docs/tutorials/discovery-queries.mdx b/docs/docs/learn/tutorials/search-and-queries/discovery-queries.mdx similarity index 95% rename from docs/docs/tutorials/discovery-queries.mdx rename to docs/docs/learn/tutorials/search-and-queries/discovery-queries.mdx index 4744e836..a7060754 100644 --- a/docs/docs/tutorials/discovery-queries.mdx +++ b/docs/docs/learn/tutorials/search-and-queries/discovery-queries.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/discovery-queries sidebar_position: 20 -title: "Discovery Queries: Exploring an Unknown Schema" +title: 'Discovery Queries: Exploring an Unknown Schema' description: A practical workflow for exploring a RushDB project you did not design — using ontology tools, label listing, and progressive query refinement before building reliable retrieval. tags: [MCP, SearchQuery, Schema, Exploration] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Discovery Queries: Exploring an Unknown Schema @@ -158,7 +159,7 @@ Before filtering, check what values actually exist for categorical fields. Use ` const statusCounts = await db.records.find({ labels: ['ORDER'], select: { - count: { $count: '*' }, + count: { $count: '*' }, status: '$record.status' }, groupBy: ['status', 'count'], @@ -391,7 +392,7 @@ console.log('Base query:', base.total) // Step 2: add first filter const withStatus = await db.records.find({ labels: ['ORDER'], - where: { status: 'shipped' }, // confirmed from Step 3 + where: { status: 'shipped' }, // confirmed from Step 3 limit: 1 }) console.log('With status filter:', withStatus.total) @@ -501,6 +502,6 @@ Ontology output reflects the current state of the database. In a live system it ## Next steps -- [Agent-Safe Query Planning with Ontology First](./agent-safe-query-planning.mdx) — automate the discovery loop in an agent -- [MCP Quickstart for Real Operators](./mcp-operator-quickstart.mdx) — the same workflow via the MCP server -- [Thinking in Graphs](./thinking-in-graphs.mdx) — mental model for working with connected data +- [Agent-Safe Query Planning with Ontology First](/tutorials/agent-safe-query-planning) — automate the discovery loop in an agent +- [MCP Quickstart for Real Operators](/deploy/mcp-operator-quickstart) — the same workflow via the MCP server +- [Thinking in Graphs](/tutorials/thinking-in-graphs) — mental model for working with connected data diff --git a/docs/docs/tutorials/query-optimization.mdx b/docs/docs/learn/tutorials/search-and-queries/query-optimization.mdx similarity index 79% rename from docs/docs/tutorials/query-optimization.mdx rename to docs/docs/learn/tutorials/search-and-queries/query-optimization.mdx index dba0abcd..d4776cf9 100644 --- a/docs/docs/tutorials/query-optimization.mdx +++ b/docs/docs/learn/tutorials/search-and-queries/query-optimization.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/query-optimization sidebar_position: 31 -title: "Query Optimization" +title: 'Query Optimization' description: Shape SearchQuery, traversal breadth, aggregation strategy, and batch patterns to reduce compute cost and improve throughput. tags: [Performance, SearchQuery, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Query Optimization @@ -16,14 +17,14 @@ RushDB executes every `find()` call as a Cypher query against Neo4j. The shape o ## Cost drivers -| Factor | Impact | -|---|---| -| Number of `where` traversal hops | Multiplied graph scan per additional hop | -| `$collect` in select | Materializes all matched records into a list | -| Missing `labels` filter | Scans all record types | -| High `limit` without indexed field ordering | Full sort on unindexed data | -| Per-record enrichment inside loops | O(n) queries for n results | -| `$nin` / `$in` with large arrays | Evaluated against every candidate record | +| Factor | Impact | +| ------------------------------------------- | -------------------------------------------- | +| Number of `where` traversal hops | Multiplied graph scan per additional hop | +| `$collect` in select | Materializes all matched records into a list | +| Missing `labels` filter | Scans all record types | +| High `limit` without indexed field ordering | Full sort on unindexed data | +| Per-record enrichment inside loops | O(n) queries for n results | +| `$nin` / `$in` with large arrays | Evaluated against every candidate record | --- @@ -86,7 +87,7 @@ Push the most selective filter as deep into the traversal as possible. This prun const broad = await db.records.find({ labels: ['CUSTOMER'], where: { - status: 'active', // customer filter + status: 'active', // customer filter ORDER: { $relation: { type: 'PLACED', direction: 'out' } } // No filter on ORDER itself — all orders are traversed } @@ -99,7 +100,7 @@ const narrow = await db.records.find({ status: 'active', ORDER: { $relation: { type: 'PLACED', direction: 'out' }, - totalUsd: { $gte: 500 } // filter at the ORDER hop + totalUsd: { $gte: 500 } // filter at the ORDER hop } } }) @@ -163,7 +164,7 @@ const orders = await db.records.find({ } }) -const orderIds = orders.data.map(o => o.__id) +const orderIds = orders.data.map((o) => o.__id) const teams = await db.records.find({ labels: ['TEAM'], @@ -343,19 +344,19 @@ def build_query(params: dict) -> dict: ## Quick-reference checklist -| Check | Why it matters | -|---|---| -| `labels` always set | Avoids full graph scan | -| Most selective filter at deepest hop | Prunes intermediate node sets early | -| Traversal depth ≤ 2 hops | Three+ hops need production benchmarking | -| `$collect` replaced with `$count` where possible | `$collect` materializes all matched IDs | -| Bulk writes via `importJson` | One round-trip instead of N | -| `limit` capped server-side | Prevents runaway result sets | +| Check | Why it matters | +| ------------------------------------------------ | ---------------------------------------- | +| `labels` always set | Avoids full graph scan | +| Most selective filter at deepest hop | Prunes intermediate node sets early | +| Traversal depth ≤ 2 hops | Three+ hops need production benchmarking | +| `$collect` replaced with `$count` where possible | `$collect` materializes all matched IDs | +| Bulk writes via `importJson` | One round-trip instead of N | +| `limit` capped server-side | Prevents runaway result sets | --- ## Next steps -- [Testing SearchQuery](./testing-searchquery.mdx) — verify query correctness across TS, Python, and REST -- [Discovery Queries](./discovery-queries.mdx) — explore unknown schemas before committing to traversal patterns -- [Hybrid Retrieval](./hybrid-retrieval.mdx) — structured filter + semantic ranking patterns +- [Testing SearchQuery](/tutorials/testing-searchquery) — verify query correctness across TS, Python, and REST +- [Discovery Queries](/tutorials/discovery-queries) — explore unknown schemas before committing to traversal patterns +- [Hybrid Retrieval](/tutorials/hybrid-retrieval) — structured filter + semantic ranking patterns diff --git a/docs/docs/tutorials/reusable-search-query.mdx b/docs/docs/learn/tutorials/search-and-queries/reusable-search-query.mdx similarity index 93% rename from docs/docs/tutorials/reusable-search-query.mdx rename to docs/docs/learn/tutorials/search-and-queries/reusable-search-query.mdx index 3a615208..ed113409 100644 --- a/docs/docs/tutorials/reusable-search-query.mdx +++ b/docs/docs/learn/tutorials/search-and-queries/reusable-search-query.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/reusable-search-query sidebar_position: 4 title: Reusable SearchQuery description: Learn the canonical SearchQuery shape reused across records, properties, labels, relationships, and values tags: [Search, Patterns, Querying] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Reusable SearchQuery @@ -158,10 +159,7 @@ where: { $or: [ { status: 'active' }, { - $and: [ - { status: 'pending' }, - { priority: { $gte: 8 } } - ] + $and: [{ status: 'pending' }, { priority: { $gte: 8 } }] } ] } @@ -390,7 +388,7 @@ curl -X POST "https://api.rushdb.com/api/v1/records/search" \ const byStatus = await db.records.find({ labels: ['ORDER'], select: { - count: { $count: '*' }, + count: { $count: '*' }, revenue: { $sum: '$record.total' } }, groupBy: ['$record.status'], @@ -726,17 +724,17 @@ Typical flow: ## API Reference Links -| Endpoint | Docs | -|---|---| -| `/api/v1/records/search` | [Records API](../rest-api/records/get-records.md) | -| `/api/v1/records/delete` | [Delete Records](../rest-api/records/delete-records.md) | -| `/api/v1/relationships/search` | [Relationships API](../rest-api/relationships.md) | -| `/api/v1/labels/search` | [Labels API](../rest-api/labels.md) | -| `/api/v1/properties/search` | [Properties API](../rest-api/properties.md) | -| `/api/v1/properties/:id/values` | [Properties API](../rest-api/properties.md) | +| Endpoint | Docs | +| ------------------------------- | -------------------------------------------------- | +| `/api/v1/records/search` | [Records API](/rest-api/records/get-records) | +| `/api/v1/records/delete` | [Delete Records](/rest-api/records/delete-records) | +| `/api/v1/relationships/search` | [Relationships API](/rest-api/relationships) | +| `/api/v1/labels/search` | [Labels API](/rest-api/labels) | +| `/api/v1/properties/search` | [Properties API](/rest-api/properties) | +| `/api/v1/properties/:id/values` | [Properties API](/rest-api/properties) | ## Next Step If you want advanced examples (nested collect, multi-hop traversal, time buckets, KPI patterns), continue with the deep-dive tutorial: -[SearchQuery Deep Dive: Advanced Patterns](./searchquery-advanced-patterns.mdx) +[SearchQuery Deep Dive: Advanced Patterns](/tutorials/searchquery-advanced-patterns) diff --git a/docs/docs/tutorials/search-ux-patterns.mdx b/docs/docs/learn/tutorials/search-and-queries/search-ux-patterns.mdx similarity index 86% rename from docs/docs/tutorials/search-ux-patterns.mdx rename to docs/docs/learn/tutorials/search-and-queries/search-ux-patterns.mdx index faf2e853..eaf3867b 100644 --- a/docs/docs/tutorials/search-ux-patterns.mdx +++ b/docs/docs/learn/tutorials/search-and-queries/search-ux-patterns.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/search-ux-patterns sidebar_position: 30 -title: "Search UX Patterns" +title: 'Search UX Patterns' description: Combine structured filters, semantic ranking, and contextual fields to build explainable, user-facing search experiences on top of RushDB. tags: [Search, AI, SearchQuery, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Search UX Patterns @@ -42,8 +43,9 @@ interface ArticleSearchParams { async function searchArticles(params: ArticleSearchParams) { const where: Record = { status: 'published' } - if (params.category) where.category = params.category - if (params.authorId) where.AUTHOR = { $relation: { type: 'AUTHORED_BY', direction: 'out' }, authorId: params.authorId } + if (params.category) where.category = params.category + if (params.authorId) + where.AUTHOR = { $relation: { type: 'AUTHORED_BY', direction: 'out' }, authorId: params.authorId } if (params.publishedAfter) where.publishedAt = { $gte: params.publishedAfter } const result = await db.records.find({ @@ -55,7 +57,7 @@ async function searchArticles(params: ArticleSearchParams) { }) return { - items: result.data.map(a => ({ + items: result.data.map((a) => ({ id: a.__id, title: a.title, category: a.category, @@ -148,17 +150,17 @@ async function semanticArticleSearch(params: SemanticSearchParams) { if (params.category || params.publishedAfter) { const where: Record = { status: 'published' } - if (params.category) where.category = params.category + if (params.category) where.category = params.category if (params.publishedAfter) where.publishedAt = { $gte: params.publishedAfter } searchParams.where = where } const result = await db.ai.search(searchParams) - return result.data.map(a => ({ + return result.data.map((a) => ({ id: a.__id, title: a.title, - score: a.__score, // 0–1 cosine similarity + score: a.__score, // 0–1 cosine similarity category: a.category })) } @@ -325,7 +327,7 @@ async function paginatedArticles(category: string, page: number, pageSize = 20): }) return { - items: result.data.map(a => ({ id: a.__id, title: a.title, publishedAt: a.publishedAt })), + items: result.data.map((a) => ({ id: a.__id, title: a.title, publishedAt: a.publishedAt })), total: result.total, hasNext: (page + 1) * pageSize < result.total } @@ -422,19 +424,19 @@ def search_with_fallback(query: str, category: str) -> dict: ## Choosing between structured and semantic search -| Situation | Use | -|---|---| -| User selects filters from UI facets | `records.find()` with `where` | -| User types a free-text query box | `ai.search()` | -| User types AND applies filters | `ai.search()` with `where` | -| Result count matters (pagination) | `records.find()` — `ai.search()` does not return `total` | -| Query is empty / filters only | `records.find()` — skip semantic scoring | -| Exact match required | `records.find()` | +| Situation | Use | +| ----------------------------------- | -------------------------------------------------------- | +| User selects filters from UI facets | `records.find()` with `where` | +| User types a free-text query box | `ai.search()` | +| User types AND applies filters | `ai.search()` with `where` | +| Result count matters (pagination) | `records.find()` — `ai.search()` does not return `total` | +| Query is empty / filters only | `records.find()` — skip semantic scoring | +| Exact match required | `records.find()` | --- ## Next steps -- [Hybrid Retrieval](./hybrid-retrieval.mdx) — two-phase structured-filter + semantic-rank for AI pipelines -- [Explainable Results](./explainable-results.mdx) — surface evidence for every search result -- [Query Optimization](./query-optimization.mdx) — shape queries for throughput and cost efficiency +- [Hybrid Retrieval](/tutorials/hybrid-retrieval) — two-phase structured-filter + semantic-rank for AI pipelines +- [Explainable Results](/tutorials/explainable-results) — surface evidence for every search result +- [Query Optimization](/tutorials/query-optimization) — shape queries for throughput and cost efficiency diff --git a/docs/docs/tutorials/searchquery-advanced-patterns.mdx b/docs/docs/learn/tutorials/search-and-queries/searchquery-advanced-patterns.mdx similarity index 97% rename from docs/docs/tutorials/searchquery-advanced-patterns.mdx rename to docs/docs/learn/tutorials/search-and-queries/searchquery-advanced-patterns.mdx index 2cbb19df..4f0781df 100644 --- a/docs/docs/tutorials/searchquery-advanced-patterns.mdx +++ b/docs/docs/learn/tutorials/search-and-queries/searchquery-advanced-patterns.mdx @@ -1,16 +1,17 @@ --- +slug: /tutorials/searchquery-advanced-patterns sidebar_position: 5 -title: "SearchQuery Deep Dive: Advanced Patterns" +title: 'SearchQuery Deep Dive: Advanced Patterns' description: Build confidence with advanced SearchQuery patterns through realistic RushDB examples tags: [Search, Deep Dive, Aggregation, Relationships] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # SearchQuery Deep Dive: Advanced Patterns -This tutorial is a practical companion to [Reusable SearchQuery](./reusable-search-query.mdx). +This tutorial is a practical companion to [Reusable SearchQuery](/tutorials/reusable-search-query). You will compose production-style queries that RushDB teams and power users rely on every day: @@ -64,7 +65,8 @@ const activeEngineeringProjects = { } const projects = await db.records.find(activeEngineeringProjects) -``` + +```` ```python @@ -82,7 +84,8 @@ active_engineering_projects = { } projects = db.records.find(active_engineering_projects) -``` +```` + @@ -197,7 +200,7 @@ const statusBreakdown = await db.records.find({ DEPARTMENT: { name: 'Engineering' } }, select: { - count: { $count: '*' }, + count: { $count: '*' }, avgBudget: { $avg: '$record.budget', $precision: 2 } }, groupBy: ['$record.status'], @@ -269,8 +272,8 @@ const kpi = await db.records.find({ }, select: { completedCount: { $count: '*' }, - totalHours: { $sum: '$record.hours' }, - avgHours: { $avg: '$record.hours', $precision: 2 } + totalHours: { $sum: '$record.hours' }, + avgHours: { $avg: '$record.hours', $precision: 2 } }, groupBy: ['completedCount', 'totalHours', 'avgHours'], orderBy: { totalHours: 'asc' } diff --git a/docs/docs/tutorials/testing-searchquery.mdx b/docs/docs/learn/tutorials/search-and-queries/testing-searchquery.mdx similarity index 96% rename from docs/docs/tutorials/testing-searchquery.mdx rename to docs/docs/learn/tutorials/search-and-queries/testing-searchquery.mdx index 095c9875..32182cc4 100644 --- a/docs/docs/tutorials/testing-searchquery.mdx +++ b/docs/docs/learn/tutorials/search-and-queries/testing-searchquery.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/testing-searchquery sidebar_position: 32 title: 'Testing SearchQuery Across TypeScript, Python, and REST' description: Write parity-driven tests that prove one query intent behaves identically across every RushDB surface. @@ -334,6 +335,6 @@ def test_rollback_does_not_persist(): ## Next steps -- [Query Optimization](./query-optimization.mdx) — reduce the cost of the queries you've confirmed are correct -- [Discovery Queries](./discovery-queries.mdx) — explore the schema before writing parity tests -- [Explainable Results](./explainable-results.mdx) — surface evidence alongside test-verified search results +- [Query Optimization](/tutorials/query-optimization) — reduce the cost of the queries you've confirmed are correct +- [Discovery Queries](/tutorials/discovery-queries) — explore the schema before writing parity tests +- [Explainable Results](/tutorials/explainable-results) — surface evidence alongside test-verified search results diff --git a/docs/docs/tutorials/audit-trails.mdx b/docs/docs/learn/tutorials/use-cases/audit-trails.mdx similarity index 97% rename from docs/docs/tutorials/audit-trails.mdx rename to docs/docs/learn/tutorials/use-cases/audit-trails.mdx index d3c7793c..ce8960e4 100644 --- a/docs/docs/tutorials/audit-trails.mdx +++ b/docs/docs/learn/tutorials/use-cases/audit-trails.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/audit-trails sidebar_position: 24 title: 'Audit Trails with Immutable Events and Derived State' description: Log business events as immutable records separate from current state so teams can reconstruct what happened, not just what is true now. @@ -460,6 +461,6 @@ Audit trails grow with every write. For high-write systems (thousands of events ## Next steps -- [Versioning Records Without Losing Queryability](./versioning-records.mdx) — complement to audit trails -- [Compliance and Retention Patterns](./compliance-retention) — expiration, archival, and redaction -- [Temporal Graphs](./temporal-graphs.mdx) — point-in-time reconstruction from state chains +- [Versioning Records Without Losing Queryability](/tutorials/versioning-records) — complement to audit trails +- [Compliance and Retention Patterns](/tutorials/compliance-retention) — expiration, archival, and redaction +- [Temporal Graphs](/tutorials/temporal-graphs) — point-in-time reconstruction from state chains diff --git a/docs/docs/tutorials/byoc-vs-managed.mdx b/docs/docs/learn/tutorials/use-cases/byoc-vs-managed.mdx similarity index 73% rename from docs/docs/tutorials/byoc-vs-managed.mdx rename to docs/docs/learn/tutorials/use-cases/byoc-vs-managed.mdx index 35cb3148..86375075 100644 --- a/docs/docs/tutorials/byoc-vs-managed.mdx +++ b/docs/docs/learn/tutorials/use-cases/byoc-vs-managed.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/byoc-vs-managed sidebar_position: 38 -title: "BYOC vs Managed vs Self-Hosted" +title: 'BYOC vs Managed vs Self-Hosted' description: Compare RushDB's three deployment models — Managed, BYOC (Bring Your Own Cloud), and Self-Hosted — with a feature matrix, decision guide, and step-by-step migration path. tags: [Deployment, BYOC, Neo4j, Self-Hosted] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # BYOC vs Managed vs Self-Hosted @@ -16,19 +17,19 @@ RushDB offers three deployment models. This page explains the trade-offs so you ## Three deployment models -| | **Managed** | **BYOC** | **Self-Hosted** | -|---|---|---|---| -| **Who runs RushDB** | RushDB Cloud | RushDB Cloud | You | -| **Your Neo4j instance** | RushDB-managed | Yours (Aura or local) | Yours | -| **Data residency** | RushDB infrastructure | Your chosen region | Your infrastructure | -| **Setup time** | < 1 minute | ~15 minutes | 30–60 minutes | -| **Ops burden** | None | Minimal | Full | -| **Raw Cypher access** | ✗ | ✓ | ✓ | -| **Custom embedding endpoint** | ✗ | ✗ | ✓ | -| **Managed billing** | RushDB plans | RushDB plans + Neo4j costs | Infrastructure costs only | -| **Backups** | Automatic | Your responsibility | Your responsibility | -| **SLA / uptime** | RushDB SLA | RushDB SLA (RushDB layer) | DIY | -| **Compliance (SOC 2, HIPAA)** | Contact sales | Achievable | Achievable | +| | **Managed** | **BYOC** | **Self-Hosted** | +| ----------------------------- | --------------------- | -------------------------- | ------------------------- | +| **Who runs RushDB** | RushDB Cloud | RushDB Cloud | You | +| **Your Neo4j instance** | RushDB-managed | Yours (Aura or local) | Yours | +| **Data residency** | RushDB infrastructure | Your chosen region | Your infrastructure | +| **Setup time** | < 1 minute | ~15 minutes | 30–60 minutes | +| **Ops burden** | None | Minimal | Full | +| **Raw Cypher access** | ✗ | ✓ | ✓ | +| **Custom embedding endpoint** | ✗ | ✗ | ✓ | +| **Managed billing** | RushDB plans | RushDB plans + Neo4j costs | Infrastructure costs only | +| **Backups** | Automatic | Your responsibility | Your responsibility | +| **SLA / uptime** | RushDB SLA | RushDB SLA (RushDB layer) | DIY | +| **Compliance (SOC 2, HIPAA)** | Contact sales | Achievable | Achievable | --- @@ -37,6 +38,7 @@ RushDB offers three deployment models. This page explains the trade-offs so you ### Managed — start here **Choose Managed when:** + - You need to be working in minutes with no infrastructure setup. - Your data residency and compliance requirements are flexible. - You'd rather not think about Neo4j capacity planning. @@ -79,12 +81,13 @@ curl -X POST https://api.rushdb.com/api/v1/records/search \ ### BYOC — bring your own Neo4j **Choose BYOC when:** + - You already have a Neo4j Aura instance (or plan to). - You need raw Cypher access for reporting or migrations. - You want your graph data to stay within your cloud account or region. - You want RushDB's API layer managed for you but want control over the graph store. -BYOC gives you a RushDB project that connects to your Neo4j credentials. See [Connect an Aura Instance](./connect-aura-instance) for the full setup guide. +BYOC gives you a RushDB project that connects to your Neo4j credentials. See [Connect an Aura Instance](/deploy/connect-aura) for the full setup guide. @@ -143,12 +146,13 @@ curl -X POST https://api.rushdb.com/api/v1/query/raw \ ### Self-Hosted — full control **Choose Self-Hosted when:** + - Your data must not leave your own infrastructure. - You need custom embedding endpoints (open-source models, private model servers). - You want to customize deployment topology (multi-region, air-gapped, private VPC). - Regulatory requirements mandate on-premises hosting. -See the [Deployment guide](./deployment) for the complete Docker Compose walkthrough. +See the [Deployment guide](/deploy/docker-remote) for the complete Docker Compose walkthrough. @@ -157,7 +161,7 @@ See the [Deployment guide](./deployment) for the complete Docker Compose walkthr import RushDB from '@rushdb/javascript-sdk' const db = new RushDB( process.env.RUSHDB_API_KEY, - { url: process.env.RUSHDB_API_URL } // points to your own instance + { url: process.env.RUSHDB_API_URL } // points to your own instance ) ``` @@ -197,14 +201,14 @@ curl -X POST "$BASE/records/search" \ The following features are gated by deployment model: -| Feature | Managed | BYOC | Self-Hosted | -|---|---|---|---| -| `POST /api/v1/query/raw` (raw Cypher) | ✗ | ✓ | ✓ | -| Custom embedding base URL | ✗ | ✗ | ✓ | -| External embedding indexes (BYOV) | ✓ | ✓ | ✓ | -| Dashboard access | ✓ | ✓ | ✓ | -| API key management | ✓ | ✓ | ✓ | -| Rate limiter configuration | ✗ | ✗ | ✓ | +| Feature | Managed | BYOC | Self-Hosted | +| ------------------------------------- | ------- | ---- | ----------- | +| `POST /api/v1/query/raw` (raw Cypher) | ✗ | ✓ | ✓ | +| Custom embedding base URL | ✗ | ✗ | ✓ | +| External embedding indexes (BYOV) | ✓ | ✓ | ✓ | +| Dashboard access | ✓ | ✓ | ✓ | +| API key management | ✓ | ✓ | ✓ | +| Rate limiter configuration | ✗ | ✗ | ✓ | --- @@ -212,7 +216,7 @@ The following features are gated by deployment model: ### Managed → BYOC -Managed and BYOC share the same RushDB API layer. There is no direct data export between Neo4j instances since BYOC uses *your* Neo4j that you provision separately. The migration flow is: +Managed and BYOC share the same RushDB API layer. There is no direct data export between Neo4j instances since BYOC uses _your_ Neo4j that you provision separately. The migration flow is: **1. Document your data shape** @@ -262,7 +266,7 @@ while (true) { } // Serialize for re-import -const payload = allRecords.map(r => ({ +const payload = allRecords.map((r) => ({ __label: 'Product', ...r })) @@ -314,7 +318,7 @@ done **3. Create a BYOC project** -In the [RushDB Dashboard](https://app.rushdb.com), create a new project using "Use my own Neo4j instance" and follow the [BYOC setup guide](./connect-aura-instance). +In the [RushDB Dashboard](https://app.rushdb.com), create a new project using "Use my own Neo4j instance" and follow the [BYOC setup guide](/deploy/connect-aura). **4. Re-import into the BYOC project** @@ -364,7 +368,7 @@ curl -s -X POST "$BASE/records/import" \ Self-hosted uses the same REST API surface as the cloud. The migration steps are: -1. [Deploy your self-hosted instance](./deployment) with `RUSHDB_SELF_HOSTED=true`. +1. [Deploy your self-hosted instance](/deploy/docker-remote) with `RUSHDB_SELF_HOSTED=true`. 2. Create a project and API key via the self-hosted dashboard at `http://your-host:3000`. 3. Export records from the cloud using the pagination pattern above. 4. Point the SDK at your self-hosted URL and re-import. @@ -373,10 +377,9 @@ Self-hosted uses the same REST API surface as the cloud. The migration steps are ```typescript -const selfHostedDb = new RushDB( - process.env.SELFHOSTED_API_KEY, - { url: 'https://rushdb.your-company.com/api/v1' } -) +const selfHostedDb = new RushDB(process.env.SELFHOSTED_API_KEY, { + url: 'https://rushdb.your-company.com/api/v1' +}) ``` @@ -433,7 +436,7 @@ Starting fresh with no ops requirements? ## Next steps -- **Get started with Managed** — [Quickstart](../get-started/quick-tutorial) -- **Set up BYOC** — [Connect an Aura Instance](./connect-aura-instance) -- **Deploy self-hosted** — [Deployment](./deployment) -- **Manage projects post-deploy** — [Self-Hosted Project Setup](./self-hosted-project-setup) +- **Get started with Managed** — [Quickstart](/get-started/quick-tutorial) +- **Set up BYOC** — [Connect an Aura Instance](/deploy/connect-aura) +- **Deploy self-hosted** — [Deployment](/deploy/docker-remote) +- **Manage projects post-deploy** — [Self-Hosted Project Setup](/deploy/self-hosted-project-setup) diff --git a/docs/docs/tutorials/compliance-retention.mdx b/docs/docs/learn/tutorials/use-cases/compliance-retention.mdx similarity index 97% rename from docs/docs/tutorials/compliance-retention.mdx rename to docs/docs/learn/tutorials/use-cases/compliance-retention.mdx index 7756b9ce..6b828a05 100644 --- a/docs/docs/tutorials/compliance-retention.mdx +++ b/docs/docs/learn/tutorials/use-cases/compliance-retention.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/compliance-retention sidebar_position: 26 title: 'Compliance and Retention Patterns' description: Implement expiration, archival, and field-level redaction for GDPR, CCPA, and other data lifecycle requirements in RushDB without breaking graph structure. @@ -516,13 +517,13 @@ curl -s -X POST "$BASE/records/search" \ 2. **Archive before deleting** — a two-phase expiry (archive → grace period → delete) satisfies audit requirements and gives you a rollback window if a record is flagged in error 3. **Redact, do not delete, when relationships matter** — deleting a USER record breaks every edge pointing to it; nulling PII fields preserves graph integrity 4. **Run expiry jobs idempotently** — use `archived: false` and date filters so re-runs are safe -5. **Keep a separate immutable EVENT record for each redaction action** — the redaction itself is an auditable fact; see [Audit Trails](./audit-trails) +5. **Keep a separate immutable EVENT record for each redaction action** — the redaction itself is an auditable fact; see [Audit Trails](/tutorials/audit-trails) 6. **Never expose `redacted: false` records in user-facing APIs without authorization checks** — filter by `redacted: false` in all end-user queries --- ## Next steps -- [Audit Trails with Immutable Events](./audit-trails) — append-only event log for every state change -- [Versioning Records Without Losing Queryability](./versioning-records) — keeping historical state alongside current state -- [Temporal Graphs](./temporal-graphs) — point-in-time graph reconstruction +- [Audit Trails with Immutable Events](/tutorials/audit-trails) — append-only event log for every state change +- [Versioning Records Without Losing Queryability](/tutorials/versioning-records) — keeping historical state alongside current state +- [Temporal Graphs](/tutorials/temporal-graphs) — point-in-time graph reconstruction diff --git a/docs/docs/tutorials/incident-response.mdx b/docs/docs/learn/tutorials/use-cases/incident-response.mdx similarity index 98% rename from docs/docs/tutorials/incident-response.mdx rename to docs/docs/learn/tutorials/use-cases/incident-response.mdx index 93998173..4a013c55 100644 --- a/docs/docs/tutorials/incident-response.mdx +++ b/docs/docs/learn/tutorials/use-cases/incident-response.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/incident-response sidebar_position: 27 title: 'Incident Response Graphs' description: Model operational incidents as graph structures to answer root cause, blast radius, and resolution timeline questions in a single query. @@ -708,6 +709,6 @@ curl -s -X POST "$BASE/records/search" \ ## Next steps -- [Supply Chain Traceability](./supply-chain-traceability) — end-to-end causal chain tracing across complex pipelines -- [Audit Trails with Immutable Events](./audit-trails) — append-only event log for every state change -- [Data Lineage](./data-lineage) — tracking data origin and transformation through a system +- [Supply Chain Traceability](/tutorials/supply-chain-traceability) — end-to-end causal chain tracing across complex pipelines +- [Audit Trails with Immutable Events](/tutorials/audit-trails) — append-only event log for every state change +- [Data Lineage](/tutorials/data-lineage) — tracking data origin and transformation through a system diff --git a/docs/docs/tutorials/is-rushdb-right-for-me.mdx b/docs/docs/learn/tutorials/use-cases/is-rushdb-right-for-me.mdx similarity index 96% rename from docs/docs/tutorials/is-rushdb-right-for-me.mdx rename to docs/docs/learn/tutorials/use-cases/is-rushdb-right-for-me.mdx index f3b6522c..e33b9785 100644 --- a/docs/docs/tutorials/is-rushdb-right-for-me.mdx +++ b/docs/docs/learn/tutorials/use-cases/is-rushdb-right-for-me.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/is-rushdb-right-for-me sidebar_position: 5 title: 'Is RushDB Right for My Problem? A Practical Decision Guide' description: A scenario-driven catalog showing seven problems RushDB is designed for, where it outperforms fragmented stacks, and where it isn't the right fit. @@ -217,7 +218,7 @@ const exposed = await db.records.find({ Different sources, different shapes, unified graph — without a single migration. -**Go deeper:** [Data Ingestion concepts](/concepts/data-ingestion) · [Research Knowledge Graph](/tutorials/research-knowledge-graph) · [Supply Chain Traceability](/tutorials/supply-chain-traceability) +**Go deeper:** [Data Ingestion concepts](/build/data/import-data) · [Research Knowledge Graph](/tutorials/research-knowledge-graph) · [Supply Chain Traceability](/tutorials/supply-chain-traceability) --- @@ -257,7 +258,7 @@ const avgOrderBySegment = await db.records.find({ }) ``` -**Go deeper:** [Select Expressions](/concepts/search/select) · [SearchQuery Advanced Patterns](/tutorials/searchquery-advanced-patterns) +**Go deeper:** [Select Expressions](/reference/select-expressions) · [SearchQuery Advanced Patterns](/tutorials/searchquery-advanced-patterns) --- @@ -343,7 +344,7 @@ servers: Authorization: Bearer ${RUSHDB_API_KEY} ``` -**Go deeper:** [MCP Server introduction](/mcp-server/introduction) · [Agent Safe Query Planning](/tutorials/agent-safe-query-planning) · [Agent Skills with OpenClaw](/tutorials/agent-skills-with-openclaw) +**Go deeper:** [MCP Server introduction](/connect/mcp) · [Agent Safe Query Planning](/tutorials/agent-safe-query-planning) · [Agent Skills with OpenClaw](/tutorials/agent-skills-with-openclaw) --- diff --git a/docs/docs/tutorials/supply-chain-traceability.mdx b/docs/docs/learn/tutorials/use-cases/supply-chain-traceability.mdx similarity index 83% rename from docs/docs/tutorials/supply-chain-traceability.mdx rename to docs/docs/learn/tutorials/use-cases/supply-chain-traceability.mdx index 13dced1f..ce1c9a2b 100644 --- a/docs/docs/tutorials/supply-chain-traceability.mdx +++ b/docs/docs/learn/tutorials/use-cases/supply-chain-traceability.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/supply-chain-traceability sidebar_position: 28 -title: "Supply Chain Traceability and Recall Analysis" +title: 'Supply Chain Traceability and Recall Analysis' description: Model suppliers, batches, products, shipments, and incidents so teams can answer upstream-impact and downstream-blast-radius questions for recalls. tags: [Domain Blueprint, Relationships, SearchQuery, TypeScript, Python] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Supply Chain Traceability and Recall Analysis @@ -31,15 +32,15 @@ graph LR INCIDENT[INCIDENT] -->|CAUSED_BY| BATCH ``` -| Label | What it represents | -|---|---| -| `SUPPLIER` | A vendor or raw material source | -| `BATCH` | A specific lot of raw material or component | -| `PRODUCTION_RUN` | A manufacturing run that consumed batches | -| `PRODUCT` | A finished product unit or SKU | -| `SHIPMENT` | A delivery fulfillment | -| `CUSTOMER` | A receiving customer or distribution center | -| `INCIDENT` | A quality or safety report | +| Label | What it represents | +| ---------------- | ------------------------------------------- | +| `SUPPLIER` | A vendor or raw material source | +| `BATCH` | A specific lot of raw material or component | +| `PRODUCTION_RUN` | A manufacturing run that consumed batches | +| `PRODUCT` | A finished product unit or SKU | +| `SHIPMENT` | A delivery fulfillment | +| `CUSTOMER` | A receiving customer or distribution center | +| `INCIDENT` | A quality or safety report | --- @@ -56,8 +57,8 @@ const db = new RushDB(process.env.RUSHDB_API_KEY!) await db.records.importJson({ label: 'SUPPLIER', data: [ - { name: 'Chem Solutions Ltd', country: 'DE', approved: true }, - { name: 'Alloy Partners Inc', country: 'US', approved: true } + { name: 'Chem Solutions Ltd', country: 'DE', approved: true }, + { name: 'Alloy Partners Inc', country: 'US', approved: true } ] }) @@ -66,7 +67,7 @@ await db.records.importJson({ data: [ { lotId: 'LOT-2025-001', material: 'polymer-Z', producedAt: '2025-01-10', quantity: 5000 }, { lotId: 'LOT-2025-002', material: 'polymer-Z', producedAt: '2025-01-17', quantity: 4800 }, - { lotId: 'LOT-2025-003', material: 'alloy-X', producedAt: '2025-01-20', quantity: 2000 } + { lotId: 'LOT-2025-003', material: 'alloy-X', producedAt: '2025-01-20', quantity: 2000 } ] }) @@ -82,7 +83,7 @@ await db.records.importJson({ label: 'PRODUCT', data: [ { sku: 'PROD-001', name: 'Widget Alpha', serialRange: 'WA-10001:WA-11000' }, - { sku: 'PROD-002', name: 'Widget Beta', serialRange: 'WB-20001:WB-20500' } + { sku: 'PROD-002', name: 'Widget Beta', serialRange: 'WB-20001:WB-20500' } ] }) @@ -169,23 +170,47 @@ const [batches, runs, products, shipments] = await Promise.all([ db.records.find({ labels: ['SHIPMENT'] }) ]) -const batchMap = Object.fromEntries(batches.data.map(b => [b.lotId, b])) -const runMap = Object.fromEntries(runs.data.map(r => [r.runId, r])) -const productMap = Object.fromEntries(products.data.map(p => [p.sku, p])) +const batchMap = Object.fromEntries(batches.data.map((b) => [b.lotId, b])) +const runMap = Object.fromEntries(runs.data.map((r) => [r.runId, r])) +const productMap = Object.fromEntries(products.data.map((p) => [p.sku, p])) // LOT-001 USED_IN RUN-A1 -await db.records.attach({ source: batchMap['LOT-2025-001'], target: runMap['RUN-A1'], options: { type: 'USED_IN', direction: 'out' } }) +await db.records.attach({ + source: batchMap['LOT-2025-001'], + target: runMap['RUN-A1'], + options: { type: 'USED_IN', direction: 'out' } +}) // LOT-002 USED_IN RUN-A2 -await db.records.attach({ source: batchMap['LOT-2025-002'], target: runMap['RUN-A2'], options: { type: 'USED_IN', direction: 'out' } }) +await db.records.attach({ + source: batchMap['LOT-2025-002'], + target: runMap['RUN-A2'], + options: { type: 'USED_IN', direction: 'out' } +}) // RUN-A1 PRODUCED PROD-001 -await db.records.attach({ source: runMap['RUN-A1'], target: productMap['PROD-001'], options: { type: 'PRODUCED', direction: 'out' } }) +await db.records.attach({ + source: runMap['RUN-A1'], + target: productMap['PROD-001'], + options: { type: 'PRODUCED', direction: 'out' } +}) // RUN-A2 PRODUCED PROD-002 -await db.records.attach({ source: runMap['RUN-A2'], target: productMap['PROD-002'], options: { type: 'PRODUCED', direction: 'out' } }) +await db.records.attach({ + source: runMap['RUN-A2'], + target: productMap['PROD-002'], + options: { type: 'PRODUCED', direction: 'out' } +}) // Products INCLUDED_IN shipments -await db.records.attach({ source: productMap['PROD-001'], target: shipments.data[0], options: { type: 'INCLUDED_IN', direction: 'out' } }) -await db.records.attach({ source: productMap['PROD-002'], target: shipments.data[1], options: { type: 'INCLUDED_IN', direction: 'out' } }) +await db.records.attach({ + source: productMap['PROD-001'], + target: shipments.data[0], + options: { type: 'INCLUDED_IN', direction: 'out' } +}) +await db.records.attach({ + source: productMap['PROD-002'], + target: shipments.data[1], + options: { type: 'INCLUDED_IN', direction: 'out' } +}) ``` @@ -458,6 +483,6 @@ Real supply chains are many-to-many: a production run may consume dozens of batc ## Next steps -- [Data Lineage](./data-lineage.mdx) — end-to-end causal chain from sources to answers -- [Audit Trails](./audit-trails.mdx) — append immutable events to supply chain records -- [Incident Response Graphs](./incident-response) — operational incident root cause analysis +- [Data Lineage](/tutorials/data-lineage) — end-to-end causal chain from sources to answers +- [Audit Trails](/tutorials/audit-trails) — append immutable events to supply chain records +- [Incident Response Graphs](/tutorials/incident-response) — operational incident root cause analysis diff --git a/docs/docs/tutorials/event-driven-ingestion.mdx b/docs/docs/learn/tutorials/working-with-data/event-driven-ingestion.mdx similarity index 97% rename from docs/docs/tutorials/event-driven-ingestion.mdx rename to docs/docs/learn/tutorials/working-with-data/event-driven-ingestion.mdx index 0ec63901..ccbfed21 100644 --- a/docs/docs/tutorials/event-driven-ingestion.mdx +++ b/docs/docs/learn/tutorials/working-with-data/event-driven-ingestion.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/event-driven-ingestion sidebar_position: 29 title: 'Event-Driven Ingestion from Webhooks and Queues' description: Handle partial, repeated, and out-of-order events from webhooks or message queues without corrupting connected graph state. @@ -422,6 +423,6 @@ def apply_order_status(order_id: str, new_status: str, event_at: str) -> None: ## Next steps -- [Audit Trails](./audit-trails.mdx) — append events to records without mutating them -- [Versioning Records](./versioning-records.mdx) — maintain a history of every state transition -- [Supply Chain Traceability](./supply-chain-traceability.mdx) — event-sourced causal chains across graph hops +- [Audit Trails](/tutorials/audit-trails) — append events to records without mutating them +- [Versioning Records](/tutorials/versioning-records) — maintain a history of every state transition +- [Supply Chain Traceability](/tutorials/supply-chain-traceability) — event-sourced causal chains across graph hops diff --git a/docs/docs/tutorials/graph-backed-api.mdx b/docs/docs/learn/tutorials/working-with-data/graph-backed-api.mdx similarity index 93% rename from docs/docs/tutorials/graph-backed-api.mdx rename to docs/docs/learn/tutorials/working-with-data/graph-backed-api.mdx index 60e6613c..93adc643 100644 --- a/docs/docs/tutorials/graph-backed-api.mdx +++ b/docs/docs/learn/tutorials/working-with-data/graph-backed-api.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/graph-backed-api sidebar_position: 18 -title: "Building a Graph-Backed API Layer" +title: 'Building a Graph-Backed API Layer' description: Expose RushDB through an application API with query translation, safe filtering, and response shaping patterns for production use. tags: [Integration, API, TypeScript, Python, Architecture] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Building a Graph-Backed API Layer @@ -77,7 +78,7 @@ function buildArticleQuery(params: ArticleSearchParams, tenantId: string): Searc labels: ['ARTICLE'], where, orderBy: { publishedAt: 'desc' }, - skip: ((params.page ?? 0) * (params.pageSize ?? 20)), + skip: (params.page ?? 0) * (params.pageSize ?? 20), limit: Math.min(params.pageSize ?? 20, 100) // cap at 100 } } @@ -100,7 +101,7 @@ export async function getArticles(req: Request): Promise { const result = await db.records.find(query) return Response.json({ - articles: result.data.map(r => ({ + articles: result.data.map((r) => ({ id: r.__id, title: r.title, category: r.category, @@ -242,13 +243,13 @@ export async function getProjectDetail(projectId: string, tenantId: string) { name: project.name, status: project.status }, - openTasks: tasksResult.data.map(t => ({ + openTasks: tasksResult.data.map((t) => ({ id: t.__id, title: t.title, dueDate: t.dueDate, status: t.status })), - members: membersResult.data.map(m => ({ + members: membersResult.data.map((m) => ({ id: m.__id, name: m.name, role: m.role @@ -350,9 +351,8 @@ Expose KPI endpoints that return counts, sums, and distributions — not raw rec ```typescript // GET /analytics/tasks-by-status?tenantId=...&projectId=... export async function getTaskStatusBreakdown(tenantId: string, projectId?: string) { - const projectFilter = projectId - ? { PROJECT: { __id: projectId, $relation: { type: 'CONTAINS', direction: 'in' } } } - : {} + const projectFilter = + projectId ? { PROJECT: { __id: projectId, $relation: { type: 'CONTAINS', direction: 'in' } } } : {} const result = await db.records.find({ labels: ['TASK'], @@ -443,7 +443,7 @@ async function previewDelete(tenantId: string, filter: Record): select: { count: { $count: '*' } }, groupBy: ['count'] }) - return { count: result.data[0]?.count as number ?? 0, dryRun: true } + return { count: (result.data[0]?.count as number) ?? 0, dryRun: true } } async function executeDelete(tenantId: string, filter: Record) { @@ -540,6 +540,6 @@ SearchQuery `where` clauses support arbitrary nesting and traversal. In a public ## Next steps -- [Hybrid Retrieval: Structured Filters Plus Semantic Search](./hybrid-retrieval.mdx) — combining filter and vector search in one handler -- [Semantic Search for Multi-Tenant Products](./semantic-search-multitenant.mdx) — tenant isolation at the storage layer -- [Query Optimization and KU Efficiency](./query-optimization.mdx) — measuring and reducing compute cost +- [Hybrid Retrieval: Structured Filters Plus Semantic Search](/tutorials/hybrid-retrieval) — combining filter and vector search in one handler +- [Semantic Search for Multi-Tenant Products](/tutorials/semantic-search-multitenant) — tenant isolation at the storage layer +- [Query Optimization and KU Efficiency](/tutorials/query-optimization) — measuring and reducing compute cost diff --git a/docs/docs/tutorials/importing-data.mdx b/docs/docs/learn/tutorials/working-with-data/importing-data.mdx similarity index 74% rename from docs/docs/tutorials/importing-data.mdx rename to docs/docs/learn/tutorials/working-with-data/importing-data.mdx index b31a9aa9..b9c4f59d 100644 --- a/docs/docs/tutorials/importing-data.mdx +++ b/docs/docs/learn/tutorials/working-with-data/importing-data.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/importing-data title: Importing data from external sources description: Learn how to import your data to RushDB sidebar_position: 3 tags: [Data, Getting Started] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Importing data from external sources @@ -17,6 +18,7 @@ This guide will help you to import your data and make it breathe: relationships, created easily. What you'll use: + - `records.createMany` (JSON and CSV import) - `relationships.createMany` (bulk linking by key match) @@ -28,10 +30,11 @@ Most external systems already have stable identifiers (MongoDB's ObjectId, HubSp When importing to RushDB, store those external IDs on your records (e.g., `mongoId`, `hubspotId`, `pgId`). Then create relationships by matching those keys using `relationships.createMany`: -1) Import data (keep external IDs as properties). -2) Create relationships by joining `source[key] = target[key]`. +1. Import data (keep external IDs as properties). +2. Create relationships by joining `source[key] = target[key]`. Safeguards and notes + - You control the relationship `type` and `direction` (default direction is `out`). - For key-based creation, provide both `source.key` and `target.key`. - Only use `manyToMany` when you explicitly want a cartesian link across filtered sets. @@ -43,6 +46,7 @@ Safeguards and notes Goal: Import MongoDB collections (e.g., `users`, `orders`) and connect them using Mongo's ObjectId values. Recommended mapping + - Persist the original `_id` as a string field on the RushDB record: `mongoId`. - For references (e.g., `orders.userId`), persist as `userMongoId` so you can join `users.mongoId = orders.userMongoId`. @@ -58,42 +62,42 @@ import { MongoClient, ObjectId } from 'mongodb' const db = new RushDB(process.env.RUSHDB_API_KEY!) async function run() { - const mongo = await MongoClient.connect(process.env.MONGO_URI!) - const mdb = mongo.db('acme') - - // 1) Extract from Mongo - const users = await mdb.collection('users').find({ tenantId: 'ACME' }).toArray() - const orders = await mdb.collection('orders').find({ tenantId: 'ACME' }).toArray() - - // 2) Normalize docs for RushDB - const usersPayload = users.map(u => ({ - mongoId: String(u._id), // keep external id as string - tenantId: u.tenantId, - name: u.name, - email: u.email - })) - - const ordersPayload = orders.map(o => ({ - mongoId: String(o._id), - tenantId: o.tenantId, - total: o.total, - // capture the referenced user id for later linking - userMongoId: String(o.userId instanceof ObjectId ? o.userId : new ObjectId(o.userId)) - })) - - // 3) Import into RushDB - await db.records.createMany({ label: 'USER', data: usersPayload }) - await db.records.createMany({ label: 'ORDER', data: ordersPayload }) - - // 4) Link: USER -[:ORDERED]-> ORDER using mongo ids - await db.relationships.createMany({ - source: { label: 'USER', key: 'mongoId', where: { tenantId: 'ACME' } }, - target: { label: 'ORDER', key: 'userMongoId', where: { tenantId: 'ACME' } }, - type: 'ORDERED', - direction: 'out' - }) - - await mongo.close() + const mongo = await MongoClient.connect(process.env.MONGO_URI!) + const mdb = mongo.db('acme') + + // 1) Extract from Mongo + const users = await mdb.collection('users').find({ tenantId: 'ACME' }).toArray() + const orders = await mdb.collection('orders').find({ tenantId: 'ACME' }).toArray() + + // 2) Normalize docs for RushDB + const usersPayload = users.map((u) => ({ + mongoId: String(u._id), // keep external id as string + tenantId: u.tenantId, + name: u.name, + email: u.email + })) + + const ordersPayload = orders.map((o) => ({ + mongoId: String(o._id), + tenantId: o.tenantId, + total: o.total, + // capture the referenced user id for later linking + userMongoId: String(o.userId instanceof ObjectId ? o.userId : new ObjectId(o.userId)) + })) + + // 3) Import into RushDB + await db.records.createMany({ label: 'USER', data: usersPayload }) + await db.records.createMany({ label: 'ORDER', data: ordersPayload }) + + // 4) Link: USER -[:ORDERED]-> ORDER using mongo ids + await db.relationships.createMany({ + source: { label: 'USER', key: 'mongoId', where: { tenantId: 'ACME' } }, + target: { label: 'ORDER', key: 'userMongoId', where: { tenantId: 'ACME' } }, + type: 'ORDERED', + direction: 'out' + }) + + await mongo.close() } run().catch(console.error) @@ -186,6 +190,7 @@ POST /api/v1/relationships/create-many ``` Common pitfalls + - Ensure you convert `ObjectId` to string when storing in RushDB; the join is string equality. - Keep tenant/workspace scoping in your `where` filters to avoid cross-tenant links. @@ -196,6 +201,7 @@ Common pitfalls Goal: Import HubSpot objects (Contacts, Companies, Deals) and connect them using HubSpot IDs. Recommended mapping + - Store the HubSpot object ID on the record (e.g., `hubspotId`). - For associations, store the associated object’s HubSpot ID on the related record (e.g., a Deal with `companyHubspotId`). @@ -212,34 +218,34 @@ const db = new RushDB(process.env.RUSHDB_API_KEY!) const hubspot = new Hubspot.Client({ accessToken: process.env.HUBSPOT_TOKEN! }) async function importHubspot() { - // 1) Fetch Contacts and Companies - const contactsRes = await hubspot.crm.contacts.basicApi.getPage(100, undefined, ['email']) - const companiesRes = await hubspot.crm.companies.basicApi.getPage(100, undefined, ['name', 'domain']) - - const contacts = contactsRes.results.map(c => ({ - hubspotId: c.id, - email: c.properties?.email, - tenantId: 'ACME' - })) - - const companies = companiesRes.results.map(co => ({ - hubspotId: co.id, - name: co.properties?.name, - domain: co.properties?.domain, - tenantId: 'ACME' - })) - - // 2) Import - await db.records.createMany({ label: 'HS_CONTACT', data: contacts }) - await db.records.createMany({ label: 'HS_COMPANY', data: companies }) - - // 3) Associate Contacts to Companies by joining HubSpot IDs - await db.relationships.createMany({ - source: { label: 'HS_CONTACT', key: 'companyHubspotId', where: { tenantId: 'ACME' } }, - target: { label: 'HS_COMPANY', key: 'hubspotId', where: { tenantId: 'ACME' } }, - type: 'WORKS_AT', - direction: 'out' - }) + // 1) Fetch Contacts and Companies + const contactsRes = await hubspot.crm.contacts.basicApi.getPage(100, undefined, ['email']) + const companiesRes = await hubspot.crm.companies.basicApi.getPage(100, undefined, ['name', 'domain']) + + const contacts = contactsRes.results.map((c) => ({ + hubspotId: c.id, + email: c.properties?.email, + tenantId: 'ACME' + })) + + const companies = companiesRes.results.map((co) => ({ + hubspotId: co.id, + name: co.properties?.name, + domain: co.properties?.domain, + tenantId: 'ACME' + })) + + // 2) Import + await db.records.createMany({ label: 'HS_CONTACT', data: contacts }) + await db.records.createMany({ label: 'HS_COMPANY', data: companies }) + + // 3) Associate Contacts to Companies by joining HubSpot IDs + await db.relationships.createMany({ + source: { label: 'HS_CONTACT', key: 'companyHubspotId', where: { tenantId: 'ACME' } }, + target: { label: 'HS_COMPANY', key: 'hubspotId', where: { tenantId: 'ACME' } }, + type: 'WORKS_AT', + direction: 'out' + }) } importHubspot().catch(console.error) @@ -320,10 +326,10 @@ Alternative: Deals to Companies ```typescript await db.relationships.createMany({ - source: { label: 'HS_DEAL', key: 'companyHubspotId', where: { tenantId: 'ACME' } }, - target: { label: 'HS_COMPANY', key: 'hubspotId', where: { tenantId: 'ACME' } }, - type: 'RELATED_TO', - direction: 'out' + source: { label: 'HS_DEAL', key: 'companyHubspotId', where: { tenantId: 'ACME' } }, + target: { label: 'HS_COMPANY', key: 'hubspotId', where: { tenantId: 'ACME' } }, + type: 'RELATED_TO', + direction: 'out' }) ``` @@ -356,6 +362,7 @@ curl -s -X POST "$BASE/relationships/create-many" \ Notes + - HubSpot v3 uses string IDs; storing them verbatim is fine for equality joins. - If you rely on HubSpot association APIs, mirror those association IDs onto one side to enable the key match. @@ -366,8 +373,9 @@ Notes Goal: Import relational tables (e.g., `users`, `orders`) and connect them using primary/foreign keys. Recommended mapping + - Store the SQL primary key as `pgId` (for `users`) and the foreign key as `userPgId` (for `orders`). - Then join `USER.pgId = ORDER.userPgId`. + Then join `USER.pgId = ORDER.userPgId`. ### Example @@ -381,30 +389,45 @@ import { Client } from 'pg' const db = new RushDB(process.env.RUSHDB_API_KEY!) async function importPg() { - const client = new Client({ connectionString: process.env.PG_URI }) - await client.connect() - - // 1) Extract - const usersRes = await client.query('select id, name, email, tenant_id from users where tenant_id = $1', ['ACME']) - const ordersRes = await client.query('select id, user_id, total, tenant_id from orders where tenant_id = $1', ['ACME']) - - // 2) Normalize - const users = usersRes.rows.map(r => ({ pgId: String(r.id), name: r.name, email: r.email, tenantId: r.tenant_id })) - const orders = ordersRes.rows.map(r => ({ pgId: String(r.id), userPgId: String(r.user_id), total: r.total, tenantId: r.tenant_id })) - - // 3) Import - await db.records.createMany({ label: 'USER', data: users }) - await db.records.createMany({ label: 'ORDER', data: orders }) - - // 4) Link: USER -[:ORDERED]-> ORDER by key equality - await db.relationships.createMany({ - source: { label: 'USER', key: 'pgId', where: { tenantId: 'ACME' } }, - target: { label: 'ORDER', key: 'userPgId', where: { tenantId: 'ACME' } }, - type: 'ORDERED', - direction: 'out' - }) - - await client.end() + const client = new Client({ connectionString: process.env.PG_URI }) + await client.connect() + + // 1) Extract + const usersRes = await client.query('select id, name, email, tenant_id from users where tenant_id = $1', [ + 'ACME' + ]) + const ordersRes = await client.query( + 'select id, user_id, total, tenant_id from orders where tenant_id = $1', + ['ACME'] + ) + + // 2) Normalize + const users = usersRes.rows.map((r) => ({ + pgId: String(r.id), + name: r.name, + email: r.email, + tenantId: r.tenant_id + })) + const orders = ordersRes.rows.map((r) => ({ + pgId: String(r.id), + userPgId: String(r.user_id), + total: r.total, + tenantId: r.tenant_id + })) + + // 3) Import + await db.records.createMany({ label: 'USER', data: users }) + await db.records.createMany({ label: 'ORDER', data: orders }) + + // 4) Link: USER -[:ORDERED]-> ORDER by key equality + await db.relationships.createMany({ + source: { label: 'USER', key: 'pgId', where: { tenantId: 'ACME' } }, + target: { label: 'ORDER', key: 'userPgId', where: { tenantId: 'ACME' } }, + type: 'ORDERED', + direction: 'out' + }) + + await client.end() } importPg().catch(console.error) @@ -480,6 +503,7 @@ curl -s -X POST "$BASE/relationships/create-many" \ ### CSV path (no code runtime) + If you export tables to CSV, you can import with REST `POST /api/v1/records/import/csv` or SDK `records.createMany`, then run the same `relationships.createMany` call as above by joining the columns you preserved (e.g., `pgId` and `userPgId`). --- @@ -499,32 +523,42 @@ const db = new RushDB(process.env.RUSHDB_API_KEY!) const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!) async function importSupabase() { - // 1) Extract - const { data: users, error: uerr } = await supabase - .from('users') - .select('id,name,email,tenant_id') - .eq('tenant_id', 'ACME') - if (uerr) throw uerr - - const { data: orders, error: oerr } = await supabase - .from('orders') - .select('id,user_id,total,tenant_id') - .eq('tenant_id', 'ACME') - if (oerr) throw oerr - - // 2) Normalize - const usersPayload = (users ?? []).map(r => ({ pgId: String(r.id), name: r.name, email: r.email, tenantId: r.tenant_id })) - const ordersPayload = (orders ?? []).map(r => ({ pgId: String(r.id), userPgId: String(r.user_id), total: r.total, tenantId: r.tenant_id })) - - // 3) Import and link - await db.records.createMany({ label: 'USER', data: usersPayload }) - await db.records.createMany({ label: 'ORDER', data: ordersPayload }) - await db.relationships.createMany({ - source: { label: 'USER', key: 'pgId', where: { tenantId: 'ACME' } }, - target: { label: 'ORDER', key: 'userPgId', where: { tenantId: 'ACME' } }, - type: 'ORDERED', - direction: 'out' - }) + // 1) Extract + const { data: users, error: uerr } = await supabase + .from('users') + .select('id,name,email,tenant_id') + .eq('tenant_id', 'ACME') + if (uerr) throw uerr + + const { data: orders, error: oerr } = await supabase + .from('orders') + .select('id,user_id,total,tenant_id') + .eq('tenant_id', 'ACME') + if (oerr) throw oerr + + // 2) Normalize + const usersPayload = (users ?? []).map((r) => ({ + pgId: String(r.id), + name: r.name, + email: r.email, + tenantId: r.tenant_id + })) + const ordersPayload = (orders ?? []).map((r) => ({ + pgId: String(r.id), + userPgId: String(r.user_id), + total: r.total, + tenantId: r.tenant_id + })) + + // 3) Import and link + await db.records.createMany({ label: 'USER', data: usersPayload }) + await db.records.createMany({ label: 'ORDER', data: ordersPayload }) + await db.relationships.createMany({ + source: { label: 'USER', key: 'pgId', where: { tenantId: 'ACME' } }, + target: { label: 'ORDER', key: 'userPgId', where: { tenantId: 'ACME' } }, + type: 'ORDERED', + direction: 'out' + }) } importSupabase().catch(console.error) @@ -605,41 +639,41 @@ import admin from 'firebase-admin' const db = new RushDB(process.env.RUSHDB_API_KEY!) admin.initializeApp({ - credential: admin.credential.applicationDefault(), - projectId: process.env.GCLOUD_PROJECT + credential: admin.credential.applicationDefault(), + projectId: process.env.GCLOUD_PROJECT }) const fs = admin.firestore() async function importFirestore() { - // 1) Fetch - const usersSnap = await fs.collection('users').where('tenantId', '==', 'ACME').get() - const ordersSnap = await fs.collection('orders').where('tenantId', '==', 'ACME').get() - - // 2) Normalize - const users = usersSnap.docs.map(d => ({ - firebaseId: d.id, - tenantId: d.get('tenantId'), - name: d.get('name'), - email: d.get('email') - })) - - const orders = ordersSnap.docs.map(d => ({ - firebaseId: d.id, - tenantId: d.get('tenantId'), - total: d.get('total'), - userFirebaseId: String(d.get('userId')) // reference to users doc id - })) - - // 3) Import and link - await db.records.createMany({ label: 'USER', data: users }) - await db.records.createMany({ label: 'ORDER', data: orders }) - await db.relationships.createMany({ - source: { label: 'USER', key: 'firebaseId', where: { tenantId: 'ACME' } }, - target: { label: 'ORDER', key: 'userFirebaseId', where: { tenantId: 'ACME' } }, - type: 'ORDERED', - direction: 'out' - }) + // 1) Fetch + const usersSnap = await fs.collection('users').where('tenantId', '==', 'ACME').get() + const ordersSnap = await fs.collection('orders').where('tenantId', '==', 'ACME').get() + + // 2) Normalize + const users = usersSnap.docs.map((d) => ({ + firebaseId: d.id, + tenantId: d.get('tenantId'), + name: d.get('name'), + email: d.get('email') + })) + + const orders = ordersSnap.docs.map((d) => ({ + firebaseId: d.id, + tenantId: d.get('tenantId'), + total: d.get('total'), + userFirebaseId: String(d.get('userId')) // reference to users doc id + })) + + // 3) Import and link + await db.records.createMany({ label: 'USER', data: users }) + await db.records.createMany({ label: 'ORDER', data: orders }) + await db.relationships.createMany({ + source: { label: 'USER', key: 'firebaseId', where: { tenantId: 'ACME' } }, + target: { label: 'ORDER', key: 'userFirebaseId', where: { tenantId: 'ACME' } }, + type: 'ORDERED', + direction: 'out' + }) } importFirestore().catch(console.error) @@ -709,6 +743,7 @@ curl -s -X POST "$BASE/relationships/create-many" \ Notes + - For multi-tenant Firestore, include a `tenantId` field and filter `where` accordingly. - If orders reference users via DocumentReference objects, resolve to `ref.id` when building the payload. @@ -729,41 +764,42 @@ const db = new RushDB(process.env.RUSHDB_API_KEY!) const base = new Airtable({ apiKey: process.env.AIRTABLE_TOKEN! }).base(process.env.AIRTABLE_BASE_ID!) async function importAirtable() { - // 1) Fetch - const companiesTable = base('Companies') - const contactsTable = base('Contacts') - - const companies = await companiesTable.select({ pageSize: 100 }).all() - const contacts = await contactsTable.select({ pageSize: 100 }).all() - - // 2) Normalize - const companiesPayload = companies.map(r => ({ - airtableId: r.id, - tenantId: 'ACME', - name: r.get('Name') as string, - domain: (r.get('Domain') as string) || undefined - })) - - const contactsPayload = contacts.map(r => ({ - airtableId: r.id, - tenantId: 'ACME', - name: r.get('Name') as string, - email: (r.get('Email') as string) || undefined, - companyAirtableId: Array.isArray(r.get('Company')) && (r.get('Company') as string[])[0] - ? (r.get('Company') as string[])[0] - : undefined - })) - - // 3) Import and link - await db.records.createMany({ label: 'AT_COMPANY', data: companiesPayload }) - await db.records.createMany({ label: 'AT_CONTACT', data: contactsPayload }) - - await db.relationships.createMany({ - source: { label: 'AT_CONTACT', key: 'companyAirtableId', where: { tenantId: 'ACME' } }, - target: { label: 'AT_COMPANY', key: 'airtableId', where: { tenantId: 'ACME' } }, - type: 'WORKS_AT', - direction: 'out' - }) + // 1) Fetch + const companiesTable = base('Companies') + const contactsTable = base('Contacts') + + const companies = await companiesTable.select({ pageSize: 100 }).all() + const contacts = await contactsTable.select({ pageSize: 100 }).all() + + // 2) Normalize + const companiesPayload = companies.map((r) => ({ + airtableId: r.id, + tenantId: 'ACME', + name: r.get('Name') as string, + domain: (r.get('Domain') as string) || undefined + })) + + const contactsPayload = contacts.map((r) => ({ + airtableId: r.id, + tenantId: 'ACME', + name: r.get('Name') as string, + email: (r.get('Email') as string) || undefined, + companyAirtableId: + Array.isArray(r.get('Company')) && (r.get('Company') as string[])[0] ? + (r.get('Company') as string[])[0] + : undefined + })) + + // 3) Import and link + await db.records.createMany({ label: 'AT_COMPANY', data: companiesPayload }) + await db.records.createMany({ label: 'AT_CONTACT', data: contactsPayload }) + + await db.relationships.createMany({ + source: { label: 'AT_CONTACT', key: 'companyAirtableId', where: { tenantId: 'ACME' } }, + target: { label: 'AT_COMPANY', key: 'airtableId', where: { tenantId: 'ACME' } }, + type: 'WORKS_AT', + direction: 'out' + }) } importAirtable().catch(console.error) @@ -843,6 +879,7 @@ curl -s -X POST "$BASE/relationships/create-many" \ Notes + - If a contact can link to multiple companies, iterate those IDs and use `records.attach` per contact, or pre-expand into multiple joinable rows. --- @@ -862,42 +899,42 @@ const db = new RushDB(process.env.RUSHDB_API_KEY!) const notion = new Client({ auth: process.env.NOTION_TOKEN! }) async function importNotion() { - const peopleDbId = process.env.NOTION_PEOPLE_DB_ID! - const tasksDbId = process.env.NOTION_TASKS_DB_ID! - - // 1) Fetch - const peopleRes = await notion.databases.query({ database_id: peopleDbId }) - const tasksRes = await notion.databases.query({ database_id: tasksDbId }) - - // 2) Normalize - const people = peopleRes.results.map(p => ({ - notionId: p.id, - tenantId: 'ACME', - name: (p as any).properties?.Name?.title?.[0]?.plain_text || 'Unknown' - })) - - const tasks = tasksRes.results.map(t => { - const props = (t as any).properties - const assignees = props?.assignee?.relation as Array<{ id: string }> | undefined - const firstAssigneeId = assignees && assignees.length ? assignees[0].id : undefined - return { - notionId: t.id, - tenantId: 'ACME', - title: props?.Name?.title?.[0]?.plain_text || 'Untitled', - assigneeNotionId: firstAssigneeId - } - }) - - // 3) Import and link (single-assignee example) - await db.records.createMany({ label: 'NT_PERSON', data: people }) - await db.records.createMany({ label: 'NT_TASK', data: tasks }) - - await db.relationships.createMany({ - source: { label: 'NT_TASK', key: 'assigneeNotionId', where: { tenantId: 'ACME' } }, - target: { label: 'NT_PERSON', key: 'notionId', where: { tenantId: 'ACME' } }, - type: 'ASSIGNED_TO', - direction: 'out' - }) + const peopleDbId = process.env.NOTION_PEOPLE_DB_ID! + const tasksDbId = process.env.NOTION_TASKS_DB_ID! + + // 1) Fetch + const peopleRes = await notion.databases.query({ database_id: peopleDbId }) + const tasksRes = await notion.databases.query({ database_id: tasksDbId }) + + // 2) Normalize + const people = peopleRes.results.map((p) => ({ + notionId: p.id, + tenantId: 'ACME', + name: (p as any).properties?.Name?.title?.[0]?.plain_text || 'Unknown' + })) + + const tasks = tasksRes.results.map((t) => { + const props = (t as any).properties + const assignees = props?.assignee?.relation as Array<{ id: string }> | undefined + const firstAssigneeId = assignees && assignees.length ? assignees[0].id : undefined + return { + notionId: t.id, + tenantId: 'ACME', + title: props?.Name?.title?.[0]?.plain_text || 'Untitled', + assigneeNotionId: firstAssigneeId + } + }) + + // 3) Import and link (single-assignee example) + await db.records.createMany({ label: 'NT_PERSON', data: people }) + await db.records.createMany({ label: 'NT_TASK', data: tasks }) + + await db.relationships.createMany({ + source: { label: 'NT_TASK', key: 'assigneeNotionId', where: { tenantId: 'ACME' } }, + target: { label: 'NT_PERSON', key: 'notionId', where: { tenantId: 'ACME' } }, + type: 'ASSIGNED_TO', + direction: 'out' + }) } importNotion().catch(console.error) @@ -984,6 +1021,7 @@ curl -s -X POST "$BASE/relationships/create-many" \ Notes + - If a Task can have multiple assignees, either: - iterate assignee IDs and call `records.attach` per Task, or - pre-expand into multiple Task rows (each with a single `assigneeNotionId`) before import to keep `createMany`-by-key workflow. @@ -1006,10 +1044,10 @@ await db.records.createMany({ label: 'ORDER', data: orders }) // Link by key equality await db.relationships.createMany({ - source: { label: 'USER', key: 'mongoId', where: { tenantId: 'ACME' } }, - target: { label: 'ORDER', key: 'userMongoId', where: { tenantId: 'ACME' } }, - type: 'ORDERED', - direction: 'out' + source: { label: 'USER', key: 'mongoId', where: { tenantId: 'ACME' } }, + target: { label: 'ORDER', key: 'userMongoId', where: { tenantId: 'ACME' } }, + type: 'ORDERED', + direction: 'out' }) ``` @@ -1074,6 +1112,6 @@ curl -s -X POST "$BASE/relationships/create-many" \ ## See also -- TypeScript SDK: [Relationships](../typescript-sdk/relationships) · [Import Data](../typescript-sdk/records/import-data) -- Python SDK: [Relationships](../python-sdk/relationships) · [Import Data](../python-sdk/records/import-data) -- REST API: [Relationships API](../rest-api/relationships) · [Records Import](../rest-api/records/import-data) +- TypeScript SDK: [Relationships](/build/graph/connect-records) · [Import Data](/build/data/import-data) +- Python SDK: [Relationships](/build/graph/connect-records) · [Import Data](/build/data/import-data) +- REST API: [Relationships API](/rest-api/relationships) · [Records Import](/rest-api/records/import-data) diff --git a/docs/docs/tutorials/importing-from-mongodb.mdx b/docs/docs/learn/tutorials/working-with-data/importing-from-mongodb.mdx similarity index 88% rename from docs/docs/tutorials/importing-from-mongodb.mdx rename to docs/docs/learn/tutorials/working-with-data/importing-from-mongodb.mdx index 6132abf6..c783a6c6 100644 --- a/docs/docs/tutorials/importing-from-mongodb.mdx +++ b/docs/docs/learn/tutorials/working-with-data/importing-from-mongodb.mdx @@ -1,12 +1,13 @@ --- +slug: /tutorials/importing-from-mongodb title: Importing from MongoDB description: A step-by-step guide to migrating MongoDB collections to RushDB — nested documents, embedded arrays, upsert, and change streams sidebar_position: 4 tags: [Data, MongoDB, Getting Started] --- -import Tabs from '@site/src/components/LanguageTabs'; -import TabItem from '@theme/TabItem'; +import Tabs from '@site/src/components/LanguageTabs' +import TabItem from '@theme/TabItem' # Importing from MongoDB @@ -45,7 +46,7 @@ MongoDB documents are rarely flat. A typical `users` document might look like: "address": { "city": "London", "country": "UK" }, "orders": [ { "_id": "64f2b...", "total": 149.99, "status": "shipped" }, - { "_id": "64f2c...", "total": 29.99, "status": "delivered" } + { "_id": "64f2c...", "total": 29.99, "status": "delivered" } ] } ``` @@ -77,7 +78,7 @@ async function bulkImport() { // ── 2. Reshape ────────────────────────────────────────────── // • Convert _id (ObjectId) to a plain string // • Name child-array keys after the label you want in RushDB - const payload = users.map(u => ({ + const payload = users.map((u) => ({ mongoId: String(u._id), name: u.name, email: u.email, @@ -181,6 +182,7 @@ curl -s -X POST "$BASE/records/import" \ After this runs, RushDB contains: + - One `User` record per MongoDB user document - One `Order` record per embedded order, automatically linked to its parent user @@ -200,7 +202,8 @@ async function incrementalSync() { // Only fetch documents updated in the last hour const since = new Date(Date.now() - 60 * 60 * 1000) - const users = await mdb.collection('users') + const users = await mdb + .collection('users') .find({ updatedAt: { $gte: since } }) .toArray() @@ -210,7 +213,7 @@ async function incrementalSync() { return } - const payload = users.map(u => ({ + const payload = users.map((u) => ({ mongoId: String(u._id), name: u.name, email: u.email, @@ -228,8 +231,8 @@ async function incrementalSync() { data: payload, options: { suggestTypes: true, - mergeBy: ['mongoId'], // match existing records by mongoId - mergeStrategy: 'append' // update changed fields, keep everything else + mergeBy: ['mongoId'], // match existing records by mongoId + mergeStrategy: 'append' // update changed fields, keep everything else } }) @@ -303,9 +306,10 @@ curl -s -X POST "$BASE/records/import" \ :::info mergeStrategy options + - **`append`** — adds/updates provided fields, keeps any others already in RushDB. Best for incremental enrichment. - **`rewrite`** — replaces all own properties with the incoming set. Best when RushDB should be an exact mirror of the source. -::: + ::: --- @@ -321,13 +325,13 @@ async function importWithReferences() { const mongo = await MongoClient.connect(process.env.MONGO_URI!) const mdb = mongo.db() - const users = await mdb.collection('users').find({}).toArray() + const users = await mdb.collection('users').find({}).toArray() const orders = await mdb.collection('orders').find({}).toArray() // 1) Import Users await db.records.createMany({ label: 'User', - data: users.map(u => ({ + data: users.map((u) => ({ mongoId: String(u._id), name: u.name, email: u.email @@ -338,9 +342,9 @@ async function importWithReferences() { // 2) Import Orders — preserve the reference as userMongoId for the join await db.records.createMany({ label: 'Order', - data: orders.map(o => ({ + data: orders.map((o) => ({ mongoId: String(o._id), - userMongoId: String(o.userId), // foreign-key reference stored as plain string + userMongoId: String(o.userId), // foreign-key reference stored as plain string total: o.total, status: o.status, createdAt: o.createdAt?.toISOString() @@ -350,7 +354,7 @@ async function importWithReferences() { // 3) Link: User -[:PLACED]-> Order by joining mongoId = userMongoId await db.relationships.createMany({ - source: { label: 'User', key: 'mongoId' }, + source: { label: 'User', key: 'mongoId' }, target: { label: 'Order', key: 'userMongoId' }, type: 'PLACED', direction: 'out' @@ -446,7 +450,7 @@ If your documents have multi-level nesting (e.g. orders containing line items co ```typescript -const payload = orders.map(o => ({ +const payload = orders.map((o) => ({ mongoId: String(o._id), total: o.total, // 'LineItem' becomes the child label; each item gets its own record @@ -455,11 +459,16 @@ const payload = orders.map(o => ({ quantity: item.qty, unitPrice: item.price, // 'Product' becomes a grandchild record under LineItem - Product: item.product ? [{ - mongoId: String(item.product._id), - name: item.product.name, - sku: item.product.sku - }] : [] + Product: + item.product ? + [ + { + mongoId: String(item.product._id), + name: item.product.name, + sku: item.product.sku + } + ] + : [] })) })) @@ -569,7 +578,7 @@ async function watchCollection() { if (event.operationType === 'update') { const doc = (event as any).fullDocument - if (!doc) return // fullDocument is null when not using updateLookup pipeline + if (!doc) return // fullDocument is null when not using updateLookup pipeline await db.records.upsert({ label: 'User', data: { @@ -589,7 +598,7 @@ async function watchCollection() { } }) - stream.on('error', err => console.error('Change stream error:', err)) + stream.on('error', (err) => console.error('Change stream error:', err)) console.log('Watching users collection for changes...') } @@ -679,21 +688,17 @@ async function importLargeCollection(batchSize = 500) { let imported = 0 while (true) { - const batch = await collection - .find({}) - .skip(skip) - .limit(batchSize) - .toArray() + const batch = await collection.find({}).skip(skip).limit(batchSize).toArray() if (!batch.length) break - const payload = batch.map(p => ({ + const payload = batch.map((p) => ({ mongoId: String(p._id), sku: p.sku, name: p.name, price: p.price, category: p.category, - tags: p.tags // string[] — will be AI-indexed if an embedding index exists + tags: p.tags // string[] — will be AI-indexed if an embedding index exists })) await db.records.createMany({ @@ -791,7 +796,7 @@ async function main() { const mdb = mongo.db('acme') const users = await mdb.collection('users').find({}).toArray() - const payload = users.map(u => ({ + const payload = users.map((u) => ({ mongoId: String(u._id), name: u.name, email: u.email, @@ -881,17 +886,17 @@ curl -s -X POST "$BASE/records/import" \ ## Troubleshooting -| Symptom | Likely cause | Fix | -|---|---|---| -| `createMany` throws "not a flat object" | Document has embedded objects or arrays | Use `records.importJson` instead | -| Child records created with wrong label | Nested array key name not matching desired label | Rename the key in the reshape step (e.g. rename `orders` → `Order`) | -| Duplicate records after re-import | `mergeBy` not set | Add `mergeBy: ['mongoId']` to options | -| Join not linking records | ObjectId not converted to string | Ensure both sides use `String(objectId)` | -| Change stream `fullDocument` is null | `updateLookup` not enabled or update is partial | Use `{ fullDocument: 'updateLookup' }` in the watch options | -| Import dies on large collections | Memory exhausted on `.toArray()` | Use cursor pagination with `.skip()` / `.limit()` batching | +| Symptom | Likely cause | Fix | +| --------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------- | +| `createMany` throws "not a flat object" | Document has embedded objects or arrays | Use `records.importJson` instead | +| Child records created with wrong label | Nested array key name not matching desired label | Rename the key in the reshape step (e.g. rename `orders` → `Order`) | +| Duplicate records after re-import | `mergeBy` not set | Add `mergeBy: ['mongoId']` to options | +| Join not linking records | ObjectId not converted to string | Ensure both sides use `String(objectId)` | +| Change stream `fullDocument` is null | `updateLookup` not enabled or update is partial | Use `{ fullDocument: 'updateLookup' }` in the watch options | +| Import dies on large collections | Memory exhausted on `.toArray()` | Use cursor pagination with `.skip()` / `.limit()` batching | ## See also -- [Importing data from external sources](./importing-data) — HubSpot, Postgres, Firebase, Airtable, Notion -- TypeScript SDK: [Import Data](../typescript-sdk/records/import-data) · [Relationships](../typescript-sdk/relationships) -- Python SDK: [Import Data](../python-sdk/records/import-data) · [Relationships](../python-sdk/relationships) +- [Importing data from external sources](/tutorials/importing-data) — HubSpot, Postgres, Firebase, Airtable, Notion +- TypeScript SDK: [Import Data](/build/data/import-data) · [Relationships](/build/graph/connect-records) +- Python SDK: [Import Data](/build/data/import-data) · [Relationships](/build/graph/connect-records) diff --git a/docs/docs/tutorials/third-party-webhook-ingestion.mdx b/docs/docs/learn/tutorials/working-with-data/third-party-webhook-ingestion.mdx similarity index 94% rename from docs/docs/tutorials/third-party-webhook-ingestion.mdx rename to docs/docs/learn/tutorials/working-with-data/third-party-webhook-ingestion.mdx index 61dc5970..895f81f0 100644 --- a/docs/docs/tutorials/third-party-webhook-ingestion.mdx +++ b/docs/docs/learn/tutorials/working-with-data/third-party-webhook-ingestion.mdx @@ -1,4 +1,5 @@ --- +slug: /tutorials/third-party-webhook-ingestion sidebar_position: 30 title: 'Webhook Ingestion from Third-Party Tools (Clay, Supabase, Superglue)' description: 'A beginner-friendly guide to send JSON or CSV from no-code and data platforms into RushDB using custom HTTP requests.' @@ -167,7 +168,7 @@ Tip: start with a single small batch, validate fields, then scale. Webhook systems may retry requests. Use an external stable ID (`externalId`) in each record so you can deduplicate and upsert safely. -For stronger idempotency patterns, see [Event-Driven Ingestion from Webhooks and Queues](./event-driven-ingestion). +For stronger idempotency patterns, see [Event-Driven Ingestion from Webhooks and Queues](/tutorials/event-driven-ingestion). ## Optional: test from terminal first @@ -193,6 +194,6 @@ curl -X POST "https://api.rushdb.com/api/v1/records/import/json" \ ## Related docs -- [Importing data from external sources](./importing-data) -- [Event-Driven Ingestion from Webhooks and Queues](./event-driven-ingestion) -- [REST API introduction](../rest-api/introduction) +- [Importing data from external sources](/tutorials/importing-data) +- [Event-Driven Ingestion from Webhooks and Queues](/tutorials/event-driven-ingestion) +- [REST API introduction](/rest-api/introduction) diff --git a/docs/docs/mcp-server/_category_.json b/docs/docs/mcp-server/_category_.json deleted file mode 100644 index 1d735d30..00000000 --- a/docs/docs/mcp-server/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "MCP Server", - "position": 6, - "collapsed": false, - "collapsible": false -} diff --git a/docs/docs/mcp-server/configuration.mdx b/docs/docs/mcp-server/configuration.mdx deleted file mode 100644 index 30f00380..00000000 --- a/docs/docs/mcp-server/configuration.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: configuration -title: Configuration -sidebar_label: Configuration -sidebar_position: 4 ---- - -# Configuration - -## Connection modes - -### Remote OAuth (ChatGPT, Claude.ai, web clients) - -No configuration file needed. Point your AI client at: - -``` -https://mcp.rushdb.com/mcp -``` - -Authentication is handled via OAuth — your client will redirect to RushDB for sign-in. No API key is required in this mode; you authorize with your RushDB account and the server issues a token scoped to the selected project. - -### Local (API key) - -For stdio-based clients (Claude Desktop, Cursor, VS Code), the MCP package is configured through environment variables: - -| Variable | Required | Default | Description | -| ---------------- | -------- | ------------------------------- | ------------------------------------------ | -| `RUSHDB_API_KEY` | **yes** | — | Your RushDB API token (from the dashboard) | -| `RUSHDB_API_URL` | no | `https://api.rushdb.com/api/v1` | Override for self-hosted or staging | - -:::tip Security -Treat `RUSHDB_API_KEY` like a password — do not commit it to version control. Prefer MCP client `env` injection or OS keychains over `.env` files in repositories. -::: - -## Providing API key values - -### 1) MCP client configuration - -```json -{ - "mcpServers": { - "rushdb": { - "command": "npx", - "args": ["-y", "@rushdb/mcp-server"], - "env": { - "RUSHDB_API_KEY": "your-rushdb-api-key-here", - "RUSHDB_API_URL": "https://api.rushdb.com/api/v1" - } - } - } -} -``` - -### 2) `.env` file - -``` -RUSHDB_API_KEY=your-rushdb-api-key-here -# Optional: -RUSHDB_API_URL=https://your-hosted/api/v1 -``` - -### 3) Shell export - -```bash -export RUSHDB_API_KEY="your-rushdb-api-key-here" -``` diff --git a/docs/docs/mcp-server/examples.mdx b/docs/docs/mcp-server/examples.mdx deleted file mode 100644 index 98ea7780..00000000 --- a/docs/docs/mcp-server/examples.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -id: examples -title: Examples -sidebar_label: Examples -sidebar_position: 5 ---- - -# Examples - -## Discovery - -Understand the data shape before querying: - -> "What labels and fields exist in my database? Use getOntologyMarkdown." - -The server calls `getOntologyMarkdown` once and returns a compact schema — label names, field types, value ranges, and the relationship map. Use exact label names from this response — they're case-sensitive. - ---- - -## Simple query - -> "Find the top 5 movies by rating." - -Calls `findRecords` with `orderBy: {rating: "desc"}, limit: 5`. - ---- - -## Select Expressions (Aggregation) - -> "How many records exist per label? Show me a breakdown." - -> "What's the average rating of MOVIE records, grouped by genre?" - - -Calls `findRecords` with `select + groupBy`. **Never include `limit` when `select` is present** — it restricts the record scan and produces incorrect totals. - -Call getSearchQuerySpec, then run findRecords with: - -```json -{ - "labels": ["Order"], - "select": { "total": { "$sum": "$record.amount" } }, - "groupBy": ["status"] -} -``` - ---- - -## Relationships - -> "Find all actors who appeared in films directed by Christopher Nolan." - -Calls `findRecords` with nested `where` traversal: ACTOR → MOVIE → DIRECTOR. - ---- - -## Bulk import - -> "I'll paste a JSON array of employee records — import them as EMPLOYEE records, upsert on email." - -Calls `bulkCreateRecords` with `mergeBy: ["email"], mergeStrategy: "append"`. - ---- - -## Semantic search - -> "Find articles most similar to 'distributed database systems'." - -Calls `semanticSearch` with the query embedded against the indexed `content` property. Requires a ready embedding index (see workflow below). - ---- - -## Embedding index workflow - -Semantic search requires a ready index. This spans 3 tool calls and is non-obvious: - -> "Create an embedding index on the `content` property of ARTICLE records, then wait until it's ready, then find articles similar to 'machine learning'." - -1. `createEmbeddingIndex` — registers the policy (backfill runs async, not instant) -2. `getEmbeddingIndexStats` — poll until `indexedRecords === totalRecords` -3. `semanticSearch` — now runs against the live index - ---- - -## Tips - -- Always start a new session with: "Load the schema with getOntologyMarkdown" -- Use exact label names from the schema — they're case-sensitive -- For select expressions, tell the AI "don't use limit" to avoid incorrect totals -- Call `getSearchQuerySpec` before any query using dates, `groupBy`, or relationship traversal diff --git a/docs/docs/mcp-server/introduction.mdx b/docs/docs/mcp-server/introduction.mdx deleted file mode 100644 index 1f0e7d85..00000000 --- a/docs/docs/mcp-server/introduction.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -id: introduction -title: MCP Server for RushDB -sidebar_label: Introduction -sidebar_position: 1 ---- - -# MCP Server - -Connect Claude, ChatGPT, Cursor, or any MCP-compatible client to your RushDB database. Type natural language; the server handles discovery, query building, and execution. - -``` -You: How many MOVIE records have a rating above 8? - -Claude: [calls getOntologyMarkdown → confirms MOVIE label and rating field] - [calls findRecords: labels=["MOVIE"], select={count:{"$count":"*"}}, where={rating:{$gt:8}}] - -Result: 1 movie matched (Inception, rating 8.8) -``` - -## Connection modes - -**Remote OAuth** — connect directly from ChatGPT, Claude.ai, or any web-based AI assistant. No local installation required. Authenticates via your RushDB account at: - -``` -https://mcp.rushdb.com/mcp -``` - -**Local (API key)** — install `@rushdb/mcp-server` in Claude Desktop, Cursor, VS Code, or any stdio-based MCP client. Provide your RushDB API key as an environment variable. - -→ [Quickstart](./quickstart) for setup instructions for both modes. - -## Mandatory workflow - -The MCP server ships with a built-in system prompt that enforces a four-step workflow: - -1. **ONTOLOGY** — call `getOntologyMarkdown` first. Returns all label names (case-sensitive), field names and types, value ranges, and the full relationship map in a single call. -2. **INTENT** — classify the request: aggregation, listing, or mutation. -3. **QUERY SPEC** — before any `findRecords` call that uses dates, aggregation, `groupBy`, or relationship traversal, call `getSearchQuerySpec`. It returns the full operator reference, both `groupBy` modes, limit rules, and annotated examples. -4. **BUILD** — use only label and field names returned from discovery. Labels are case-sensitive. Never invent names or operators. - -This workflow prevents hallucinated labels, wrong operator names, and common pagination errors with aggregations. - -## Prompts API - -The system prompt is also accessible via the MCP Prompts API: - -- Name: `rushdb.queryBuilder` - -Clients that support Prompts can fetch it at session start and inject it as the system message. Clients that do not yet support it can call the `getQueryBuilderPrompt` tool for the same text. - -→ [Quickstart](./quickstart) diff --git a/docs/docs/mcp-server/quickstart.mdx b/docs/docs/mcp-server/quickstart.mdx deleted file mode 100644 index 34ae4ffb..00000000 --- a/docs/docs/mcp-server/quickstart.mdx +++ /dev/null @@ -1,129 +0,0 @@ ---- -id: quickstart -title: Quickstart -sidebar_label: Quickstart -sidebar_position: 2 ---- - -# Quickstart - -Two ways to connect: - -- **Remote OAuth** — connect directly from ChatGPT, Claude.ai, or any web-based AI client with no local installation. Uses `https://mcp.rushdb.com/mcp` and authenticates via your RushDB account. -- **Local (API key)** — install the MCP package in Claude Desktop, Cursor, VS Code, or any stdio-based client. Requires a RushDB API key. - ---- - -## ChatGPT - -Connect RushDB to ChatGPT as a connector — no local installation required: - -1. In ChatGPT, open **Settings → Connectors → Add connector** -2. Enter the MCP URL: `https://mcp.rushdb.com/mcp` -3. Complete the OAuth sign-in with your RushDB account -4. Select the project you want to expose to ChatGPT - -After linking, ChatGPT can query and write to your RushDB project directly. - -:::tip Troubleshooting -If you see `OAuth linked, action discovery failed`, see the [Troubleshooting](./troubleshooting) page. -::: - ---- - -## Claude.ai - -1. In Claude.ai, open **Settings → Integrations → Add integration** -2. Enter the MCP URL: `https://mcp.rushdb.com/mcp` -3. Authorize with your RushDB account via OAuth -4. The integration is now active across Claude.ai conversations - ---- - -## Claude Desktop - -Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "rushdb": { - "command": "npx", - "args": ["-y", "@rushdb/mcp-server"], - "env": { - "RUSHDB_API_KEY": "your-api-key-here" - } - } - } -} -``` - -Then restart Claude Desktop. That's it. - -## Verify - -Ask Claude: - -> "Call getOntologyMarkdown and show me what labels exist in my RushDB project." - -## Other clients - -**Cursor** — add to `.cursor/mcp.json`: - -```json -{ - "mcpServers": { - "rushdb": { - "command": "npx", - "args": ["-y", "@rushdb/mcp-server"], - "env": { "RUSHDB_API_KEY": "your-api-key-here" } - } - } -} -``` - -**VS Code** (Copilot agent mode) — add to `.vscode/mcp.json`: - -```json -{ - "servers": { - "rushdb": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@rushdb/mcp-server"], - "env": { "RUSHDB_API_KEY": "your-api-key-here" } - } - } -} -``` - -**Override API URL** (self-hosted / staging): add `"RUSHDB_API_URL": "https://your-host/api/v1"` to any `env` block above. - -## Remote MCP endpoint - -The hosted endpoint for all remote and web-based clients: - -``` -https://mcp.rushdb.com/mcp -``` - -Do not use the root domain (`https://mcp.rushdb.com`) — always include `/mcp`. - -Local tunnel testing: `https://.ngrok-free.app/mcp` - -OAuth discovery metadata is available at: - -- `https://mcp.rushdb.com/.well-known/openid-configuration` -- `https://mcp.rushdb.com/.well-known/jwks.json` - -## Enable the Query Builder prompt (recommended) - -To make discovery-first behavior automatic: - -1. Call `ListPrompts` → find `rushdb.queryBuilder` -2. Call `GetPrompt` with that name -3. Set the returned text as the system message for your RushDB session - -No Prompts API support? Use the fallback: - -> "Call `getQueryBuilderPrompt` and set the response as the system message." diff --git a/docs/docs/mcp-server/tools.mdx b/docs/docs/mcp-server/tools.mdx deleted file mode 100644 index 64e8bdb6..00000000 --- a/docs/docs/mcp-server/tools.mdx +++ /dev/null @@ -1,699 +0,0 @@ ---- -id: tools -title: Tools -sidebar_label: Tools -sidebar_position: 4 ---- - -The RushDB MCP server exposes a comprehensive set of tools. Each tool includes a `name`, `description`, `inputSchema`, `outputSchema`, and client-facing annotations. Tool calls return both human-readable text and structured MCP `structuredContent`, so clients can render or reuse returned records, totals, CSV exports, ontology data, and operation metadata without parsing text. - -## Database discovery - -- getOntologyMarkdown — Return all labels, properties, value ranges, and relationships as Markdown (call first at session start) -- getOntology — Return ontology as structured JSON (use when property `id` values are needed for `propertyValues`) -- findLabels — List/filter record labels and counts -- findProperties — List/filter properties by name, type, or label -- findRelationships — Search for relationships - -## Record operations - -- createRecord — Create a new record -- updateRecord — Update an existing record (partial) -- setRecord — Replace all fields of a record -- deleteRecord — Delete a record (alias of deleteRecordById) -- deleteRecordById — Delete a record by ID -- getRecord — Get a record by ID -- getRecordsByIds — Get multiple records by their IDs -- findRecords — Search for records with where/limit/skip/orderBy/select/groupBy -- findOneRecord — Find a single record matching criteria -- findUniqRecord — Find a unique record matching criteria - -## Relationship management - -- attachRelation — Attach relationships between records -- detachRelation — Detach relationships between records -- findRelationships — Search for relationships - -## Bulk operations - -- bulkCreateRecords — Create multiple records at once -- bulkDeleteRecords — Delete multiple records by query - -## Data export - -- exportRecords — Export records to CSV format - -## Vector search & embeddings - -- findEmbeddingIndexes — List all embedding index policies and their status -- createEmbeddingIndex — Create an embedding index for a property (managed or external) -- upsertEmbeddingVectors — Write pre-computed vectors to an external embedding index -- deleteEmbeddingIndex — Delete an embedding index and all stored vectors -- getEmbeddingIndexStats — Return backfill progress stats for an embedding index -- semanticSearch — Perform vector similarity search over indexed records - -## Utilities - -- getSearchQuerySpec — Fetch the complete SearchQuery operator and syntax reference -- getQueryBuilderPrompt — Return the system prompt (fallback for clients without Prompts API) -- helpAddToClient — Setup instructions for adding this server to MCP clients - ---- - -## Examples - -### Find labels - -Ask your MCP client: - -> Use the RushDB MCP server to run findLabels with limit=10. - -### Create a record - -> Use createRecord to add a record with label "Task" and data `{"title": "Write docs", "status": "open"}`. - -### Find records - -> Call findRecords where `{"status": "open"}` orderBy `{"createdAt": "desc"}` limit 5. - -### Attach relationships - -> attachRelation from sourceId `""` to targetIds `["", ""]` with relationType `"references"`. - ---- - -For detailed input schemas, see the tool definitions in the MCP server source (`packages/mcp-server/tools.ts`). - -Output schemas are also defined in `packages/mcp-server/tools.ts`. The most common shapes are: - -| Tools | Structured output | -| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| `findRecords`, `getRecordsByIds`, `semanticSearch` | `{ data, total }` | -| `createRecord` | `{ success, id, message, record }` | -| `updateRecord`, `setRecord` | `{ success, message, record }` | -| `bulkCreateRecords` | `{ message, ids, data, total }` | -| `findLabels`, `findProperties`, `findRelationships` | Named arrays such as `{ labels }`, `{ properties }`, `{ relationships }` | -| Delete/export/utility tools | Operation-specific fields such as `{ success, message }`, `{ csv, dateTime, message }`, `{ markdown }`, `{ spec }`, or `{ prompt }` | - -Some write tools are annotated as destructive for MCP clients. For example, `setRecord` replaces all fields on a record, and `detachRelation` removes relationships. - ---- - -## Tool reference - -:::tip Start here -Call `getOntologyMarkdown` before any other tool. It returns all label names (case-sensitive), field names, and the full relationship map — everything you need to build accurate queries. -::: - -### getOntologyMarkdown - -Return the full database schema as Markdown: all label names (case-sensitive), their properties with types and value ranges, the complete relationship map, and a **Semantic Search** column per property that shows `sourceType similarityFunction dimensionsd [status]` (e.g. `managed cosine 1536d [ready]`) for indexed properties, or `—` when none. A non-`—` value means the property is queryable with `aiSemanticSearch`. - -**Call this first** at the start of every session before calling any find or mutation tool. The returned label and field names are the single source of truth — never invent names from context. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------ | ---------------- | -------- | ------- | ------------------------------------------------------------------------------- | -| labels | array of strings | no | — | Restrict output to specific labels; omit for the full schema | -| force | boolean | no | — | Pass `true` to bypass the 1-hour ontology cache and force a fresh recalculation | - -Example prompt: - -> Call `getOntologyMarkdown` to load the full schema before running any queries. - ---- - -### getOntology - -Return the same ontology data as structured JSON instead of Markdown. Each property may include a `vectorIndexes` array — non-empty when embedding indexes exist for that property, indicating it is queryable with `aiSemanticSearch`. Each entry has: `id`, `sourceType`, `similarityFunction`, `dimensions`, `status`, `modelKey`. Use this variant when you need property `id` values for a subsequent `propertyValues` call. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------ | ---------------- | -------- | ------- | ------------------------------------------------------------------------------- | -| labels | array of strings | no | — | Restrict output to specific labels | -| force | boolean | no | — | Pass `true` to bypass the 1-hour ontology cache and force a fresh recalculation | - -Example prompt: - -> Call `getOntology` with `labels=["Product"]` to get property IDs for the Product schema. - ---- - -### findLabels - -List or filter record labels and their counts. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ------ | -------- | ------- | -------------------------------------------------------------- | -| where | object | no | — | Filter conditions for labels (e.g., by activity flags, counts) | -| limit | number | no | — | Maximum number of labels to return | -| skip | number | no | — | Number of labels to skip | -| orderBy | object | no | — | Sorting: key = field, value = `asc` or `desc` | - -Example prompt: - -> Run `findLabels` with `limit=10` ordered by `{ "count": "desc" }`. - ---- - -### createRecord - -Create a new record with the specified label and data. - -Arguments: - -| Name | Type | Required | Default | Description | -| --------------------- | ---------------- | -------- | ---------------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| label | string | yes | — | Label for the record | -| data | object | yes | — | Record data to insert | -| transactionId | string | no | — | Optional transaction ID for atomic creation | -| options.mergeStrategy | `append | rewrite` | no | append | Upsert strategy: `append` keeps unspecified existing properties; `rewrite` replaces existing property relations | -| options.mergeBy | array of strings | no | all keys (if empty array provided) | Fields used to match an existing record. Presence (even empty array) triggers upsert semantics | - -Upsert behavior: - -- Provide either `options.mergeStrategy` or `options.mergeBy` (or both) to switch from pure create to upsert. -- If `mergeBy` is omitted and `mergeStrategy` is present, all incoming keys are used to match. -- Empty `mergeBy: []` explicitly means "use all keys". -- `append` merges new properties while retaining existing unspecified ones; `rewrite` deletes prior property value relationships first. - -Example prompt: - -> Call `createRecord` with `label="User"`, `data={"email":"a@b.com","name":"Ann"}`, and `options={"mergeBy":["email"],"mergeStrategy":"append"}` to upsert by email. - -Structured output: `{ success, id, message, record }`, where `record` is the created or upserted record data. - ---- - -### updateRecord - -Partially update fields of an existing record. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------- | ------ | -------- | ------- | -------------------------- | -| recordId | string | yes | — | ID of the record to update | -| label | string | yes | — | Label for the record | -| data | object | yes | — | Partial data to update | -| transactionId | string | no | — | Optional transaction ID | - -Example prompt: - -> Use `updateRecord` with `recordId=""`, `label="Task"`, and `data={"status":"done"}`. - -Structured output: `{ success, message, record }`, where `record` is the updated record data. - ---- - -### setRecord - -Replace all fields of a record (full update). - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------- | ------ | -------- | ------- | ------------------------------------------- | -| recordId | string | yes | — | ID of the record to set | -| label | string | yes | — | Label for the record | -| data | object | yes | — | Full record data (replaces existing fields) | -| transactionId | string | no | — | Optional transaction ID | - -Example prompt: - -> Call `setRecord` for `recordId=""`, `label="Task"`, data `{"title":"Polish docs","status":"in-progress"}`. - -Structured output: `{ success, message, record }`, where `record` is the replacement record data. - ---- - -### deleteRecord / deleteRecordById - -Delete a record by its ID. `deleteRecord` is an alias of `deleteRecordById`. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------- | ------ | -------- | ------- | -------------------------- | -| recordId | string | yes | — | ID of the record to delete | -| transactionId | string | no | — | Optional transaction ID | - -Example prompt: - -> Use `deleteRecordById` with `recordId=""`. - ---- - -### getRecord - -Retrieve a specific record by ID. - -Arguments: - -| Name | Type | Required | Default | Description | -| -------- | ------ | -------- | ------- | ---------------------------- | -| recordId | string | yes | — | ID of the record to retrieve | - -Example prompt: - -> Call `getRecord` for `recordId=""`. - ---- - -### getRecordsByIds - -Retrieve multiple records by an array of IDs. - -Arguments: - -| Name | Type | Required | Default | Description | -| --------- | ---------------- | -------- | ------- | -------------------------- | -| recordIds | array of strings | yes | — | IDs of records to retrieve | - -Example prompt: - -> Call `getRecordsByIds` with `recordIds=["",""]`. - ---- - -### findRecords - -Search for records with advanced filtering, sorting, paging, grouping, and aggregation. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ---------------- | -------- | ------- | --------------------------------------------------------------------------------------- | -| labels | array of strings | no | — | Filter by record labels | -| where | object | no | — | Query conditions | -| limit | number | no | 10 | Max records to return | -| skip | number | no | 0 | Records to skip | -| orderBy | object | no | — | Sorting: key = field, value = `asc` or `desc` | -| select | object | no | — | Output-shaping expressions: `{ $sum, $avg, $count, $min, $max, $collect, $timeBucket }` | -| groupBy | array of strings | no | — | Fields to group by (records only) | - -Example prompt: - -> Run `findRecords` with `where={"status":"open"}`, `orderBy={"createdAt":"desc"}`, `limit=5`. - -Structured output: `{ data, total }`. - ---- - -### findOneRecord - -Find a single record that matches the criteria (returns one or none). - -Arguments: - -| Name | Type | Required | Default | Description | -| ------ | ---------------- | -------- | ------- | ---------------- | -| labels | array of strings | no | — | Filter by labels | -| where | object | no | — | Query conditions | - -Example prompt: - -> Use `findOneRecord` with `where={"email":"user@example.com"}`. - ---- - -### findUniqRecord - -Find a unique record that matches the criteria (errors if multiple match in some clients; the server returns not-found text if none). - -Arguments: - -| Name | Type | Required | Default | Description | -| ------ | ---------------- | -------- | ------- | -------------------------------------------------- | -| labels | array of strings | no | — | Filter by labels | -| where | object | no | — | Query conditions expected to match a single record | - -Example prompt: - -> Use `findUniqRecord` with `labels=["User"]` and `where={"username":"alice"}`. - ---- - -### attachRelation - -Create relationships from a source record to one or more target records. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------- | ---------------- | -------- | -------------- | ---------------------------------------------------------- | -------- | ------------------------- | -| sourceId | string | yes | — | ID of the source record | -| targetId | string | no | — | ID of a single target (deprecated if `targetIds` provided) | -| targetIds | array of strings | no | — | IDs of multiple targets | -| relationType | string | no | — | Relationship type label | -| direction | `outgoing | incoming | bidirectional` | no | outgoing | Direction of the relation | -| transactionId | string | no | — | Optional transaction ID | - -Example prompt: - -> Call `attachRelation` from `sourceId=""` to `targetIds=["",""]` with `relationType="references"`. - ---- - -### detachRelation - -Remove relationships between records. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------- | ---------------- | -------- | -------------- | ---------------------------------------------------------- | -------- | ---------------- | -| sourceId | string | yes | — | ID of the source record | -| targetId | string | no | — | ID of a single target (deprecated if `targetIds` provided) | -| targetIds | array of strings | no | — | IDs of multiple targets | -| relationType | string | no | — | Relationship type to remove | -| direction | `outgoing | incoming | bidirectional` | no | outgoing | Direction filter | -| transactionId | string | no | — | Optional transaction ID | - -Example prompt: - -> Use `detachRelation` with `sourceId=""`, `targetId=""`, and `relationType="references"`. - ---- - -### findRelationships - -Search for relationships with filters, sorting, and paging. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ------ | -------- | ------- | --------------------------------------------- | -| where | object | no | — | Query conditions for relationships | -| limit | number | no | 10 | Max number to return | -| skip | number | no | 0 | Number to skip | -| orderBy | object | no | — | Sorting: key = field, value = `asc` or `desc` | - -Example prompt: - -> Run `findRelationships` with `limit=20`. - ---- - -### bulkCreateRecords - -Create multiple records in a single operation. - -Arguments: - -| Name | Type | Required | Default | Description | -| --------------------- | ---------------- | -------- | ------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| label | string | yes | — | Label for all records | -| data | array of objects | yes | — | Array of record payloads (flat objects use batch create; nested objects use JSON import path) | -| transactionId | string | no | — | Optional transaction ID | -| options.mergeStrategy | `append | rewrite` | no | append | Upsert strategy applied globally: `append` merges, `rewrite` replaces existing property relations | -| options.mergeBy | array of strings | no | all keys (if empty array) | Global match fields; empty array means all keys per record; presence triggers upsert | -| options.returnResult | boolean | no | true | Return created/upserted records (IDs always returned separately) | - -Upsert notes: - -- Same semantics as `createRecord`, but applied across the batch. -- If records are flat objects, uses the `createMany` path; otherwise falls back to JSON import BFS with upsert. -- For large batches consider reducing the size or increasing transaction TTL if timeouts occur. - -Example prompt: - -> Call `bulkCreateRecords` for `label="User"` with `data=[{"email":"a@b.com","name":"Ann"},{"email":"b@c.com","name":"Bill"}]` and `options={"mergeBy":["email"],"mergeStrategy":"append"}`. - -Structured output: `{ message, ids, data, total }`, where `data` contains the created or upserted record data. - ---- - -### bulkDeleteRecords - -Delete multiple records matching a query. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------- | ---------------- | -------- | ------- | ------------------------------ | -| labels | array of strings | no | — | Filter by labels | -| where | object | yes | — | Criteria for records to delete | -| transactionId | string | no | — | Optional transaction ID | - -Example prompt: - -> Use `bulkDeleteRecords` with `where={"status":"obsolete"}`. - ---- - -### exportRecords - -Export records to CSV. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ---------------- | -------- | ------- | -------------------------------------------------------- | -| labels | array of strings | no | — | Filter by labels | -| where | object | no | — | Criteria for records to export | -| limit | number | no | — | Max records to export | -| orderBy | object | no | — | Sorting for export: key = field, value = `asc` or `desc` | - -Example prompt: - -> Call `exportRecords` with `labels=["Task"]` and `limit=100`. - ---- - -### propertyValues - -Get values for a specific property. - -Arguments: - -| Name | Type | Required | Default | Description | -| ---------- | ------ | -------- | ------- | --------------------------- | ------------------ | -| propertyId | string | yes | — | ID of the property | -| query | string | no | — | Filter values (search term) | -| orderBy | `asc | desc` | no | — | Ordering of values | -| limit | number | no | — | Max number of values | -| skip | number | no | — | Number of values to skip | - -Example prompt: - -> Run `propertyValues` with `propertyId=""` and `limit=20`. - ---- - -### findProperties - -Search for properties with filters, sorting, and paging. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ------ | -------- | ------- | --------------------------------------------- | -| where | object | no | — | Filter conditions | -| limit | number | no | 10 | Max properties to return | -| skip | number | no | 0 | Number to skip | -| orderBy | object | no | — | Sorting: key = field, value = `asc` or `desc` | - -Example prompt: - -> Call `findProperties` with `limit=25` ordered by `{ "count": "desc" }`. - ---- - -### findPropertyById - -Retrieve a specific property by ID. - -Arguments: - -| Name | Type | Required | Default | Description | -| ---------- | ------ | -------- | ------- | ------------------ | -| propertyId | string | yes | — | ID of the property | - -Example prompt: - -> Use `findPropertyById` with `propertyId=""`. - ---- - -### deleteProperty - -Delete a property by ID. - -Arguments: - -| Name | Type | Required | Default | Description | -| ---------- | ------ | -------- | ------- | ---------------------------- | -| propertyId | string | yes | — | ID of the property to delete | - -Example prompt: - -> Call `deleteProperty` with `propertyId=""`. - -Structured output: `{ success, message, property }`, where `property` is the deleted property metadata returned by the API. - ---- - -### getSearchQuerySpec - -Return the complete RushDB SearchQuery specification as a focused reference document. Call this before building any `findRecords` query that involves dates, aggregation, `groupBy`, or relationship traversal. - -Covers: - -- All `where` operators: string, number, boolean, datetime component objects, `$exists`, `$type`, logical grouping (`$and`/`$or`/`$not`/`$nor`/`$xor`) -- Relationship traversal syntax: `$alias`, `$relation`, `$id` -- All `select` expressions: `$count`/`$sum`/`$avg`/`$min`/`$max`/`$collect`/`$timeBucket` -- Both `groupBy` modes: dimensional (one row per distinct value) and self-group (collapse to single KPI row) -- Late-ordering rules for correct full-scan totals -- `COLLECT` nesting for hierarchical output -- Limit rules by query mode, multi-hop path discovery, enum normalization -- Validation checklist and annotated query examples - -Arguments: none - -Example prompt: - -> Call `getSearchQuerySpec` to load the query syntax reference, then build a `findRecords` aggregation query. - ---- - -### getQueryBuilderPrompt - -Return the RushDB system prompt as plain text. Returns the same text that is delivered automatically via the MCP Prompts API. Use this if your MCP client does not support the Prompts API. - -Arguments: none - -Example prompt: - -> Call `getQueryBuilderPrompt` and use the response as the system message for your session. - ---- - -### helpAddToClient - -Return setup instructions for adding the RushDB MCP server to a specific MCP client (Claude Desktop, Cursor, VS Code, etc.). - -Arguments: none - -Example prompt: - -> Call `helpAddToClient` to see how to add the RushDB MCP server to Claude Desktop. - ---- - -### findEmbeddingIndexes - -List all embedding index policies configured for the current project. Call this before creating a new index to verify whether one already exists for the target label and property. - -Each returned entry contains: `id`, `label`, `propertyName`, `modelKey`, `dimensions`, `enabled`, `status` (`pending` | `indexing` | `ready` | `error`), `createdAt`, `updatedAt`. - -Arguments: none - -Example prompt: - -> Call `findEmbeddingIndexes` to see which vector indexes exist and their current status. - ---- - -### createEmbeddingIndex - -Create a new embedding index policy for a property. For **managed** indexes (default), RushDB asynchronously embeds every existing value and keeps new values embedded on write. For **external** indexes (`sourceType: "external"`), the client supplies pre-computed vectors via `upsertEmbeddingVectors`. Once the index status becomes `"ready"` (monitor with `getEmbeddingIndexStats`), use `semanticSearch` to query. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------------ | ------ | -------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| label | string | yes | — | Record label to scope the index to (e.g. `"Book"`, `"Task"`) | -| propertyName | string | yes | — | Name of the string property whose values will be embedded | -| sourceType | string | no | `"managed"` | `"managed"` (RushDB generates embeddings) or `"external"` (client provides vectors) | -| similarityFunction | string | no | `"cosine"` | `"cosine"` or `"euclidean"` | -| dimensions | number | no | auto | Vector dimensionality. **Required** for external indexes. For managed indexes, defaults to the server-configured model dimensions. | - -Example prompt: - -> Call `createEmbeddingIndex` with `label="Article"` and `propertyName="body"` to enable semantic search on article bodies. - ---- - -### upsertEmbeddingVectors - -Write pre-computed embedding vectors to an **external** vector index for a set of records. Only valid for indexes created with `sourceType: "external"`. Each vector must contain exactly as many dimensions as the index was created with. After upserting, call `getEmbeddingIndexStats` to check if all records are indexed. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ------ | -------- | ------- | ---------------------------------------------------------------- | -| indexId | string | yes | — | ID of the external embedding index (from `findEmbeddingIndexes`) | -| items | array | yes | — | Array of `{ recordId: string, vector: number[] }` pairs to write | - -Example prompt: - -> Call `upsertEmbeddingVectors` with `indexId=""` and `items=[{ recordId: "abc", vector: [0.1, 0.2, ...] }]` to write pre-computed embeddings. - ---- - -### deleteEmbeddingIndex - -Delete an embedding index policy by its ID and strip all stored embedding vectors for that index. **Irreversible.** Always confirm with the user before calling. Use `findEmbeddingIndexes` to get the `indexId`. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ------ | -------- | ------- | ----------------------------------- | -| indexId | string | yes | — | ID of the embedding index to delete | - -Example prompt: - -> Call `deleteEmbeddingIndex` with `indexId=""` after confirming the user wants to remove the index. - ---- - -### getEmbeddingIndexStats - -Return Neo4j-level statistics for an embedding index: `totalRecords` and `indexedRecords`. Use this to monitor backfill progress after creating an index — when `indexedRecords === totalRecords` the index is fully ready for semantic search. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------- | ------ | -------- | ------- | ------------------------------------------------------- | -| indexId | string | yes | — | ID of the embedding index (from `findEmbeddingIndexes`) | - -Example prompt: - -> Call `getEmbeddingIndexStats` with `indexId=""` to check how many records have been indexed so far. - ---- - -### semanticSearch - -Perform semantic (vector) similarity search over records whose `propertyName` has been indexed with `createEmbeddingIndex`. - -- **Managed indexes**: provide a free-text `query` — RushDB embeds it and returns the most similar records ranked by similarity (`__score`). -- **External indexes**: provide a `queryVector` (pre-computed `number[]`) instead of query text. - -Direct vector-index mode (fast, default) is used when no `where` filter is supplied. Prefilter mode (exact, slower) is activated when a `where` filter is supplied — candidates are first narrowed then ranked. - -Requires an embedding index in `"ready"` status for the given `label` + `propertyName`. - -Arguments: - -| Name | Type | Required | Default | Description | -| ------------------ | ---------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------- | -| propertyName | string | yes | — | Name of the indexed property to search against | -| query | string | no | — | Free-text query (managed indexes). Mutually exclusive with `queryVector` | -| queryVector | number[] | no | — | Pre-computed embedding vector (external indexes). Mutually exclusive with `query` | -| labels | array of strings | yes | — | One or more record labels to scope the search; first label resolves the embedding index | -| sourceType | string | no | — | `"managed"` or `"external"`. Required when both index types exist for the same label+property | -| similarityFunction | string | no | — | `"cosine"` or `"euclidean"`. Disambiguates when multiple indexes share the same label+property+sourceType | -| where | object | no | — | Optional filter applied before scoring (activates prefilter mode) | -| topK | number | no | 20 | Max candidates from the vector index (direct mode only) | -| limit | number | no | 20 | Maximum number of results to return | -| skip | number | no | 0 | Number of results to skip for pagination | - -Example prompt: - -> Call `semanticSearch` with `propertyName="body"`, `query="machine learning for beginners"`, and `labels=["Article"]` to find the most relevant articles. diff --git a/docs/docs/mcp-server/troubleshooting.mdx b/docs/docs/mcp-server/troubleshooting.mdx deleted file mode 100644 index 8c197df4..00000000 --- a/docs/docs/mcp-server/troubleshooting.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -id: troubleshooting -title: Troubleshooting -sidebar_label: Troubleshooting -sidebar_position: 6 ---- - -# Troubleshooting - -## HTTP errors - -The MCP server maps HTTP status codes to actionable messages returned directly to the model: - -| Status | Message returned to LLM | -| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| 400 | Bad request — query or payload is invalid. Check field names, operators, and required arguments. Call `getSearchQuerySpec` for correct syntax. | -| 401 | Unauthorized — API key is missing or invalid. | -| 403 | Forbidden — the API key lacks permission for this operation. | -| 404 | Not found — resource does not exist. Verify record IDs and label names (case-sensitive). Call `getOntologyMarkdown` to rediscover the schema. | -| 409 | Conflict — operation conflicts with existing data (e.g. duplicate key). | -| 422 | Unprocessable — field types or required fields don't match the schema. | -| 5xx | Server error — retry or contact support. | - -:::tip Self-correction -Because the model receives an actionable hint rather than a bare status code, it can self-correct — for example, calling `getOntologyMarkdown` automatically after a 404 without you needing to intervene. -::: - ---- - -## Common issues - -**ChatGPT connector: `OAuth linked, action discovery failed`** - -Often appears with: - -- `MCP_ACTION_DISCOVERY_FAILED` -- `Reauthentication required` - -Checklist: - -1. Connector URL must include `/mcp`: - - Production: `https://mcp.rushdb.com/mcp` - - Local tunnel: `https://.ngrok-free.app/mcp` -2. Verify discovery and probe endpoints: - - `/.well-known/openid-configuration` returns 200 JSON - - `/.well-known/jwks.json` returns a non-empty `keys` array - - `/mcp` GET probe returns `text/event-stream` -3. If OAuth env vars changed, restart both the OAuth issuer and MCP server. -4. Remove and recreate the ChatGPT connector to force a fresh OAuth token. - ---- - -**Missing API key** - -``` -RUSHDB_API_KEY environment variable is required. -``` - -Fix: set `RUSHDB_API_KEY` in your MCP client `env` block, a `.env` file, or via `export`. - ---- - -**Wrong or unreachable API URL** (`Invalid URL`, `Failed to fetch`, `Network error`) - -Fix: verify `RUSHDB_API_URL` (if set). Remove it to fall back to `https://api.rushdb.com/api/v1`. - ---- - -**"RushDB is not a constructor"** - -Cause: CJS/ESM interop issue in some environments. - -Fix: update to the latest `@rushdb/mcp-server` version and rebuild from source if needed. - ---- - -**Permissions or timeouts** - -- Check network/firewall configuration -- Try smaller `limit` values on large queries - ---- - -Still stuck? Open an issue at https://github.com/rush-db/rushdb/issues diff --git a/docs/docs/python-sdk/_category_.json b/docs/docs/python-sdk/_category_.json deleted file mode 100644 index 2f819ce6..00000000 --- a/docs/docs/python-sdk/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Python SDK", - "position": 10, - "collapsed": true, - "collapsible": true -} diff --git a/docs/docs/python-sdk/ai/_category_.json b/docs/docs/python-sdk/ai/_category_.json deleted file mode 100644 index 1c831adf..00000000 --- a/docs/docs/python-sdk/ai/_category_.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "label": "AI & Vectors", - "position": 1, - "collapsed": false, - "collapsible": true, - "link": { - "type": "doc", - "id": "python-sdk/ai/overview" - } -} diff --git a/docs/docs/python-sdk/ai/advanced-indexing.md b/docs/docs/python-sdk/ai/advanced-indexing.md deleted file mode 100644 index 71f85d2e..00000000 --- a/docs/docs/python-sdk/ai/advanced-indexing.md +++ /dev/null @@ -1,229 +0,0 @@ ---- -sidebar_position: 2 -title: Advanced Indexing — BYOV ---- - -# Advanced Indexing — Bring Your Own Vectors - -**External indexes** (BYOV — Bring Your Own Vectors) let you supply pre-computed embedding vectors instead of having the server compute them. Use them when you need: - -- A custom or private model the server cannot access -- Multimodal embeddings (image, audio, document structure) -- Vectors already produced by your ML pipeline -- Reproducible embeddings not tied to the server's active model - ---- - -## Creating an external index - -Pass `"sourceType": "external"` in the params dict. `dimensions` is **required** because the server never calls an embedding model and cannot infer the vector size: - -```python -# Explicit sourceType -response = db.ai.indexes.create({ - "label": "Article", - "propertyName": "body", - "sourceType": "external", - "dimensions": 768, - "similarityFunction": "cosine", -}) -print(response.data["status"]) # 'awaiting_vectors' -``` - -An external index starts with status `awaiting_vectors` and transitions to `ready` once at least one vector has been written. - -### External vs managed comparison - -| | Managed | External | -| ----------------------------- | -------------------------------- | -------------------------------------------- | -| `sourceType` | `"managed"` | `"external"` | -| Initial status | `"pending"` | `"awaiting_vectors"` | -| Who computes embeddings | RushDB server (configured model) | Your application | -| `dimensions` required | No (uses server default) | **Yes** | -| Backfill for existing records | Automatic | Manual via `upsert_vectors` or inline writes | - ---- - -## Upsert Vectors - -`db.ai.indexes.upsert_vectors()` - -The bulk upload API — ideal for seeding an index from a dataset or syncing after a batch pipeline. - -```python -db.ai.indexes.upsert_vectors( - index_id: str, - params: dict # {"items": [{"recordId": str, "vector": list[float]}]} -) -> ApiResponse -``` - -```python -# Fetch your records and embed them with your own model -records_response = db.records.find({"where": {"__label": "Article"}}) - -items = [] -for record in records_response.data: - vector = my_embedder.embed(record["body"]) # your embedding model - items.append({"recordId": record["__id"], "vector": vector}) - -db.ai.indexes.upsert_vectors(ext_index_id, {"items": items}) -``` - -The request is **idempotent** — calling it again with the same `recordId` replaces the stored vector. - ---- - -## Writing vectors at record creation time - -Instead of a two-step create → upsert_vectors flow, you can write vectors inline using the `vectors` parameter on any write operation. See [Write Records with Vectors](./write-with-vectors.md) for the full reference. - -```python -# One step: create record AND write its vector -record = db.records.create( - label="Article", - data={"title": "Warp drives", "body": "Alcubierre metric..."}, - vectors=[{"propertyName": "body", "vector": my_embedder.embed("Alcubierre metric...")}], -) -``` - ---- - -## Disambiguation {#disambiguation} - -When the same `(label, propertyName)` pair is covered by more than one external index (different `similarityFunction` or `dimensions`), specify `similarityFunction` to resolve which index to use: - -```python -# Two indexes: Article:body/cosine and Article:body/euclidean - -# ✅ Explicit — writes to the cosine index only -db.records.create( - label="Article", - data={"title": "Widget", "body": "..."}, - vectors=[{ - "propertyName": "body", - "vector": vec, - "similarityFunction": "cosine", # required when ambiguous - }], -) - -# ✅ Explicit — searches the euclidean index only -db.ai.search({ - "labels": ["Article"], - "propertyName": "body", - "queryVector": vec, - "similarityFunction": "euclidean", -}) - -# ❌ Missing similarityFunction when two indexes exist → 422 Unprocessable Entity -db.records.create( - label="Article", - data={"title": "Gadget"}, - vectors=[{"propertyName": "body", "vector": vec}], # ambiguous! -) -``` - -### Index signature uniqueness - -Two index policies are considered **identical** (and a second `create` returns `409 Conflict`) when all five fields match: - -| Field | Effect on uniqueness | -| -------------------- | -------------------- | -| `label` | ✅ | -| `propertyName` | ✅ | -| `sourceType` | ✅ | -| `similarityFunction` | ✅ | -| `dimensions` | ✅ | - -Changing any one field produces a distinct index and both are allowed to coexist. - ---- - -## Complete BYOV worked example - -```python -from rushdb import RushDB - -db = RushDB("your-api-key") - -# 1. Create the external index -idx_response = db.ai.indexes.create({ - "label": "Doc", - "propertyName": "content", - "sourceType": "external", - "dimensions": 3, - "similarityFunction": "cosine", -}) -ext_index_id = idx_response.data["id"] -# status: 'awaiting_vectors' - -# 2. Create records + write inline vectors (one round trip per record) -articles = [ - {"title": "Alpha", "content": "First article", "vector": [1, 0, 0]}, - {"title": "Beta", "content": "Second article", "vector": [0, 1, 0]}, - {"title": "Gamma", "content": "Third article", "vector": [0, 0, 1]}, -] - -for article in articles: - db.records.create( - label="Doc", - data={"title": article["title"], "content": article["content"]}, - vectors=[{"propertyName": "content", "vector": article["vector"]}], - ) - -# 3. Search using a pre-computed query vector -results = db.ai.search({ - "labels": ["Doc"], - "propertyName": "content", - "queryVector": [1, 0, 0], # closest to Alpha - "limit": 3, -}) - -print(results.data[0]["title"]) # 'Alpha' -print(results.data[0].score) # ~1.0 -``` - ---- - -## Batch import with `create_many` - -For bulk seeding with flat rows, use `db.records.create_many()` with the top-level `vectors` parameter: - -```python -db.records.create_many( - label="Doc", - data=[ - {"title": "Alpha", "content": "First article"}, - {"title": "Beta", "content": "Second article"}, - {"title": "Gamma", "content": "Third article"}, - ], - vectors=[ - [{"propertyName": "content", "vector": [1, 0, 0]}], - [{"propertyName": "content", "vector": [0, 1, 0]}], - [{"propertyName": "content", "vector": [0, 0, 1]}], - ], -) -``` - -For nested JSON payloads, use `import_json` to create records and then call `db.ai.indexes.upsert_vectors()` to seed the vectors separately. - ---- - -## Mixing managed and external indexes - -You can have both a managed index and an external index on the same property simultaneously: - -```python -# Managed — server embeds for full-text search -db.ai.indexes.create({"label": "Product", "propertyName": "description"}) - -# External — your custom multimodal model -db.ai.indexes.create({ - "label": "Product", - "propertyName": "description", - "sourceType": "external", - "dimensions": 512, - "similarityFunction": "cosine", -}) -``` - -Specify `similarityFunction` in `db.ai.search()` to route the query to the intended index. diff --git a/docs/docs/python-sdk/ai/indexing.md b/docs/docs/python-sdk/ai/indexing.md deleted file mode 100644 index 72a24e60..00000000 --- a/docs/docs/python-sdk/ai/indexing.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -sidebar_position: 1 -title: Embedding Indexes ---- - -# Embedding Indexes - -An **embedding index** is a policy that tells RushDB to vectorize a specific string property for a label. Once `status` is `ready`, every record matching that label+property pair is searchable via `db.ai.search()`. - ---- - -## How indexes work - -Indexes are scoped to `(label, propertyName)`. `Book:description` and `Article:description` are completely independent — they maintain separate vector stores and never interfere. - -``` -Index policy - label: "Book" - propertyName: "description" - sourceType: "managed" - dimensions: 1536 - status: "ready" - -↓ backfill runs automatically - -Book records get vectors stored on their VALUE relationships: - rel._emb_managed_cosine_1536 = [0.1, 0.2, ...] -``` - -When new records are created or existing records are updated, the index transitions back to `pending` and vectors are recomputed on the next backfill cycle. - ---- - -## List Indexes - -`db.ai.indexes.find()` - -List all embedding index policies for the current project. - -```python -response = db.ai.indexes.find() -for index in response.data: - print(f"{index['label']}.{index['propertyName']} — {index['status']}") -``` - -### Example response data - -```python -[ - { - "id": "idx_abc123", - "projectId": "proj_xyz", - "label": "Article", - "propertyName": "description", - "sourceType": "managed", - "similarityFunction": "cosine", - "modelKey": "text-embedding-3-small", - "dimensions": 1536, - "vectorPropertyName": "_emb_managed_cosine_1536", - "enabled": True, - "status": "ready", - "createdAt": "2025-01-10T12:00:00.000Z", - "updatedAt": "2025-01-10T12:05:00.000Z", - } -] -``` - ---- - -## Create Index - -`db.ai.indexes.create()` - -Create a new embedding index policy for a string property. - -```python -db.ai.indexes.create(params: dict) -> ApiResponse[dict] -``` - -| `params` key | Type | Required | Description | -| -------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------- | -| `label` | string | **yes** | Label to scope this index to (e.g. `"Article"`) | -| `propertyName` | string | **yes** | Name of the property to embed (e.g. `"description"`) | -| `sourceType` | string | no | `"managed"` (default) or `"external"`. See [Advanced Indexing](./advanced-indexing.md). | -| `similarityFunction` | string | no | `"cosine"` (default) or `"euclidean"` | -| `dimensions` | number | no | Vector dimensionality. Defaults to server `RUSHDB_EMBEDDING_DIMENSIONS`. **Required** for external indexes. | - -```python -# Simplest form — uses server-configured model and dimensions -response = db.ai.indexes.create({ - "label": "Article", - "propertyName": "description" -}) -print(response.data["status"]) # 'pending' → backfill starts immediately - -# With explicit parameters -response = db.ai.indexes.create({ - "label": "Article", - "propertyName": "description", - "similarityFunction": "cosine", - "dimensions": 1536 -}) -``` - -> Attempting to create a duplicate `(label, propertyName, sourceType, similarityFunction, dimensions)` tuple returns `409 Conflict`. - -### Index lifecycle - -| Status | Description | -| ------------------ | ------------------------------------------------------ | -| `pending` | Policy created, waiting for backfill scheduler | -| `indexing` | Backfill in progress | -| `awaiting_vectors` | External index — waiting for client to push vectors | -| `ready` | All existing records have vectors; search is available | -| `error` | Backfill failed; check server logs for the cause | - ---- - -## Get Index Stats - -`db.ai.indexes.stats(index_id)` - -Returns the fill rate for an index — useful for progress monitoring or health checks. - -```python -response = db.ai.indexes.stats(index_id) -stats = response.data -print(f"{stats['indexedRecords']} / {stats['totalRecords']} records indexed") -``` - ---- - -## Delete Index - -`db.ai.indexes.delete(index_id)` - -Remove an embedding index policy. The underlying Neo4j DDL vector index is only dropped when **zero embeddings remain** across the entire project. - -```python -db.ai.indexes.delete(index_id) -``` - ---- - -## Waiting for an index to become ready - -For managed indexes, backfill runs asynchronously. Poll `db.ai.indexes.find()` until `status` is `ready`: - -```python -import time - -def wait_for_index_ready(db, index_id, timeout_s=90): - deadline = time.time() + timeout_s - while time.time() < deadline: - response = db.ai.indexes.find() - idx = next((i for i in response.data if i["id"] == index_id), None) - if idx and idx["status"] == "ready": - return - if idx and idx["status"] == "error": - raise RuntimeError("Index entered error state") - time.sleep(3) - raise TimeoutError("Index did not become ready in time") - -response = db.ai.indexes.create({"label": "Book", "propertyName": "description"}) -wait_for_index_ready(db, response.data["id"]) -# now safe to call db.ai.search(...) -``` - ---- - -## Multiple indexes on the same property - -You can have more than one index per `(label, propertyName)` pair, provided the signature differs: - -```python -# Cosine index -db.ai.indexes.create({ - "label": "Product", - "propertyName": "description", - "similarityFunction": "cosine", - "dimensions": 768, -}) - -# Euclidean index on the same property -db.ai.indexes.create({ - "label": "Product", - "propertyName": "description", - "similarityFunction": "euclidean", - "dimensions": 768, -}) -``` - -When searching or writing vectors against a property with multiple indexes, specify `similarityFunction` to disambiguate. See [Advanced Indexing](./advanced-indexing.md#disambiguation) for details. - ---- - -## Index shape - -```python -{ - "id": str, - "projectId": str, - "label": str, - "propertyName": str, - "modelKey": str, - "sourceType": str, # 'managed' | 'external' - "similarityFunction": str, # 'cosine' | 'euclidean' - "dimensions": int, - "vectorPropertyName": str, # internal Neo4j property name for the vector - "enabled": bool, - "status": str, # 'pending' | 'indexing' | 'awaiting_vectors' | 'ready' | 'error' - "createdAt": str, - "updatedAt": str, -} -``` - ---- - -## String List Properties - -`List[str]` - -String list properties are supported. Each item in the list is embedded individually, then mean-pooled into a single vector stored on the relationship. diff --git a/docs/docs/python-sdk/ai/overview.md b/docs/docs/python-sdk/ai/overview.md deleted file mode 100644 index 82e8b75f..00000000 --- a/docs/docs/python-sdk/ai/overview.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -sidebar_position: 0 -title: Overview ---- - -# AI & Semantic Search - -RushDB is a **self-aware memory layer for agents, humans, and apps**. It continuously understands its own structure — labels, fields, value distributions, relationships — and exposes that knowledge so agents can reason over real data without hallucinating schema details, and apps can retrieve semantically relevant context on demand. - -The `db.ai` namespace covers three capabilities: - -| Capability | Description | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| **Graph Ontology** | Self-describing schema discovery: label names, field types, value ranges, and the relationship map — always up to date | -| **Embedding Indexes** | Per-label vector policies that turn string properties into long-term semantic memory | -| **Semantic Search** | Cosine/euclidean similarity retrieval over indexed properties, for agents and apps alike | - ---- - -## How it fits together - -``` -┌─────────────────────────────────────────────────────┐ -│ Your data (records + relationships) │ -│ │ -│ BOOK { title: "...", description: "..." } │ -└────────────────────┬────────────────────────────────┘ - │ - db.ai.indexes.create() - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Embedding index policy │ -│ label: BOOK property: description dims: 1536 │ -│ sourceType: managed | external │ -└────────────────────┬────────────────────────────────┘ - │ - Backfill (managed) / inline vectors (external) - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Vector stored on VALUE relationship │ -│ rel._emb_managed_cosine_1536 = [0.1, 0.2, ...] │ -└────────────────────┬────────────────────────────────┘ - │ - db.ai.search({ query / queryVector }) - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Records ranked by similarity score │ -│ result.get('__score') == 0.94 (cosine sim.) │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## Quick links - -| Topic | Description | -| -------------------------------------------------- | -------------------------------------------------------------- | -| [Ontology](#graph-ontology) | Schema discovery with `get_ontology_markdown` / `get_ontology` | -| [Indexing](./indexing.md) | Create and manage managed embedding indexes | -| [Advanced Indexing — BYOV](./advanced-indexing.md) | Bring Your Own Vectors: external indexes, inline writes | -| [Semantic Search](./search.md) | Query by meaning with `db.ai.search()` | -| [Writing with Vectors](./write-with-vectors.md) | Attach vectors at create / upsert / import_json time | -| [Agent Skills](#agent-skills) | Installable skills that teach any compatible agent to use RushDB | - ---- - -## Graph Ontology - -The ontology methods expose a live snapshot of your database structure — without any manual schema definitions. - -### Get Ontology as Markdown - -`db.ai.get_ontology_markdown()` - -Returns the full schema as compact Markdown — the **recommended format for LLM context injection**. - -```python -db.ai.get_ontology_markdown( - params: dict | None = None, # {"labels": ["Order"]} to scope output - # {"force": True} to bypass the 1-hour cache - transaction=None -) -> ApiResponse[str] -``` - -```python -from rushdb import RushDB - -db = RushDB("RUSHDB_API_KEY") - -# Inject into LLM at session start -response = db.ai.get_ontology_markdown() -schema = response.data - -messages = [ - {"role": "system", "content": f"You are a data assistant.\n\n{schema}"}, - {"role": "user", "content": "How many paid orders are there?"} -] - -# Scope to specific labels -order_response = db.ai.get_ontology_markdown({"labels": ["Order"]}) - -# Bypass the 1-hour cache and force a fresh recalculation -fresh_response = db.ai.get_ontology_markdown({"force": True}) -``` - -
-Example output - -```text -# Graph Ontology - -## Labels - -| Label | Count | -|-----------|------:| -| `Order` | 1840 | -| `User` | 312 | -| `Product` | 95 | - ---- - -## `Order` (1840 records) - -### Properties - -| Property | Type | Values / Range | Semantic Search | -|-------------|----------|----------------------------------------|--------------------------------| -| `status` | string | `pending`, `paid`, `shipped` (+2 more) | — | -| `total` | number | `4.99`..`2499.00` | — | -| `name` | string | `Widget A`, `Widget B` (+8 more) | `managed` cosine 1536d [ready] | -| `createdAt` | datetime | `2024-01-03`..`2026-02-27` | — | - -### Relationships - -| Type | Direction | Other Label | -|-------------|-----------|-------------| -| `PLACED_BY` | out | `User` | -| `CONTAINS` | out | `Product` | -``` - -
- ---- - -### Get Ontology (raw) - -`db.ai.get_ontology()` - -Returns the same ontology as a structured list of dicts — useful for schema UIs, auto-complete, or looking up property IDs for `db.properties.values()`. - -```python -db.ai.get_ontology( - params: dict | None = None, # {"labels": ["Order"]} to scope output - # {"force": True} to bypass the 1-hour cache - transaction=None -) -> ApiResponse[list[dict]] -``` - -```python -# List all labels with counts -response = db.ai.get_ontology() -for item in response.data: - print(f"{item['label']}: {item['count']} records") - -# Look up property ID for value enumeration -response = db.ai.get_ontology({"labels": ["Order"]}) -order_schema = response.data[0] -status_prop = next(p for p in order_schema["properties"] if p["name"] == "status") - -values_response = db.properties.values({"id": status_prop["id"]}) -# ['pending', 'paid', 'shipped', 'cancelled', 'refunded'] - -# Identify semantically-searchable properties -indexed = [p for p in order_schema["properties"] if p.get("vectorIndexes")] -# indexed[0]["vectorIndexes"][0]["status"] == "ready" → queryable with db.ai.search() - -# Bypass the 1-hour cache -fresh = db.ai.get_ontology({"force": True}) -``` - -Each item in `response.data`: - -```python -{ - "label": str, - "count": int, - "properties": [ - { - "id": str, # use with db.properties.values() - "name": str, - "type": str, # 'string' | 'number' | 'boolean' | 'datetime' - "values": list, # up to 10 samples (string/boolean only) - "min": str | float | None, # number/datetime only - "max": str | float | None, - # non-empty when embedding indexes exist — property is queryable with db.ai.search() - "vectorIndexes": [ - { - "id": str, - "sourceType": str, # 'managed' | 'external' - "similarityFunction": str, # 'cosine' | 'euclidean' - "dimensions": int, - "status": str, # 'pending' | 'indexing' | 'ready' | 'error' - "modelKey": str, - } - ], # omitted (or empty list) when no index exists - } - ], - "relationships": [ - { - "label": str, - "type": str, - "direction": str, # 'in' | 'out' - } - ] -} -``` - -:::note Caching -Both methods share a **1-hour cache** per project. The first call after TTL expiry triggers a full graph scan; all subsequent calls within the hour are instant. Pass `{"force": True}` in `params` to bypass the cache and trigger an immediate recalculation. -::: - -:::tip Agent quickstart -Call `db.ai.get_ontology_markdown()` first in every AI session. Without it, models will hallucinate field and label names. -::: - ---- - -## Agent Skills - -`@rushdb/skills` is a collection of [Agent Skills](https://agentskills.io) — installable instructions that teach any skills-compatible AI agent (Claude, GitHub Copilot, Cursor, Windsurf, and others) to use RushDB efficiently, without manual system prompt engineering. - -```bash -npx skills add rush-db/rushdb --path packages/skills -``` - -| Skill | What it teaches | -|---|---| -| `rushdb-query-builder` | Discovery-first workflow, SearchQuery syntax, aggregation, relationship traversal, and semantic search | -| `rushdb-agent-memory` | Using RushDB as persistent structured memory — store, link, and semantically recall sessions, decisions, and entities | -| `rushdb-data-modeling` | LMPG model design, label/property naming conventions, nested JSON import, and schema evolution | -| `rushdb-faceted-search` | Build faceted filter UIs — discover properties and types, enumerate distinct values, map to widgets, assemble a live `where` clause | - -Each skill bundles a `SKILL.md` with concise instructions and optional reference files (like the full SearchQuery spec) that the agent loads on demand. - -:::note MCP server vs. Agent Skills -The [MCP server](/mcp-server/introduction) gives agents direct tool access to RushDB at runtime. Agent Skills teach agents *how* to use those tools correctly — they complement each other. -::: diff --git a/docs/docs/python-sdk/ai/search.md b/docs/docs/python-sdk/ai/search.md deleted file mode 100644 index 9fe7d3f7..00000000 --- a/docs/docs/python-sdk/ai/search.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -sidebar_position: 3 -title: Semantic Search ---- - -# Semantic Search - -`db.ai.search()` performs semantic vector search across records that have an associated embedding index. - ---- - -## Signature - -```python -db.ai.search(params: dict) -> ApiResponse[list[Record]] -``` - -| `params` key | Type | Required | Description | -|----------------------|----------------------------|--------------|-----------------------------------------------------------------------------------------------------| -| `propertyName` | string | **yes** | The indexed property to search against (e.g. `"description"`) | -| `labels` | string or list of strings | **yes** | Label(s) to search within (min 1) | -| `query` | string | conditionally | Free-text query to embed. Required for managed indexes; **not allowed** for external indexes. | -| `queryVector` | list of floats | conditionally | Pre-computed query vector. Required for external indexes. Also accepted for managed indexes (bypasses server embedding). | -| `similarityFunction` | string | no | `"cosine"` or `"euclidean"`. Required when multiple indexes target the same `(label, propertyName)`. | -| `dimensions` | number | no | Disambiguates when multiple indexes match. Inferred from `len(queryVector)` when `queryVector` is supplied. | -| `where` | dict | no | Standard RushDB filter expression applied **before** similarity scoring. | -| `skip` | number | no | Pagination offset (default `0`) | -| `limit` | number | no | Maximum results to return (default `20`) | - ---- - -## Result shape - -Results are `Record` objects ordered by `__score` descending (closest match first). Access fields via `.id`, `.get()`, or `['key']`: - -```python -for result in response.data: - result.id # RushDB record ID (str) - result.get('__label') # Record label (str) - result.get('__score') # Similarity score, 0–1 (float) - result.get('title') # Your fields via .get() - result['description'] # Or via [] — raises KeyError if missing -``` - ---- - -## Managed search (query text) - -For a **managed** index, pass `query` — a natural-language string. The server embeds it using the same model that built the index. - -```python -response = db.ai.search({ - "propertyName": "description", - "query": "machine learning for beginners", - "labels": ["Article"], - "limit": 5, -}) - -for result in response.data: - print(f"[{result.get('__score'):.3f}] {result.get('title')}") -``` - ---- - -## External search (query vector) - -For an **external** index, pass `queryVector` — a pre-computed embedding produced by your own model. No text is sent to an embedding model. - -```python -vec = my_embedder.embed("machine learning for beginners") - -response = db.ai.search({ - "propertyName": "body", - "queryVector": vec, - "labels": ["Article"], - "limit": 10, -}) -``` - -- `query` is **not allowed** with external indexes. -- `queryVector` is **not required** for managed indexes but is accepted (bypasses server embedding). -- When `queryVector` is supplied, `dimensions` can be omitted — the server infers it from `len(queryVector)`. - ---- - -## Filtering with `where` - -The `where` clause acts as a **prefilter** — only records satisfying the filter are candidates for similarity ranking. All `where` operators supported by `db.records.find()` are available here. - -```python -response = db.ai.search({ - "propertyName": "description", - "query": "wireless headphones", - "labels": ["Product"], - "where": { - "category": {"$eq": "electronics"}, - "inStock": {"$eq": True}, - "price": {"$lt": 100}, - }, - "limit": 20, -}) -``` - ---- - -## Multi-label search - -Pass a list of labels to search across multiple entity types simultaneously: - -```python -response = db.ai.search({ - "propertyName": "body", - "query": "machine learning trends", - "labels": ["Article", "Post", "Comment"], - "limit": 10, -}) - -# Each result carries __label so you can tell them apart -for result in response.data: - print(result.get('__label'), f"{result.get('__score'):.3f}", result.get('title') or result.get('text')) -``` - -All listed labels must have an embedding index on the same `propertyName`, or the request returns `404` for the missing labels. - ---- - -## Disambiguation - -When two indexes exist for the same `(label, propertyName)`, specify `similarityFunction` to select the target index: - -```python -# Two indexes: Product:embedding/cosine and Product:embedding/euclidean -response = db.ai.search({ - "labels": ["Product"], - "propertyName": "embedding", - "queryVector": vec, - "similarityFunction": "cosine", # required — otherwise 422 -}) -``` - ---- - -## Pagination - -```python -PAGE = 20 - -# Page 1 -page1 = db.ai.search({ - "propertyName": "description", - "query": "sustainable packaging", - "labels": ["Product"], - "limit": PAGE, - "skip": 0, -}) - -# Page 2 -page2 = db.ai.search({ - "propertyName": "description", - "query": "sustainable packaging", - "labels": ["Product"], - "limit": PAGE, - "skip": PAGE, -}) -``` - ---- - -## Full example: AI agent with semantic search - -```python -from rushdb import RushDB, Record - -db = RushDB("RUSHDB_API_KEY") - -def build_agent_system_prompt() -> str: - schema = db.ai.get_ontology_markdown().data - return f"You are a data assistant for RushDB.\n\n{schema}" - -def semantic_search(query: str, label: str, limit: int = 5) -> list[Record]: - response = db.ai.search({ - "propertyName": "description", - "query": query, - "labels": [label], - "limit": limit, - }) - return response.data - -# Retrieve context then pass to LLM -results = semantic_search("climate change research", "Article") -for r in results: - print(f"[{r.get('__score'):.3f}] {r.get('title')}") -``` - ---- - -## Error reference - -| HTTP | Cause | -|------|-------| -| `404 Not Found` | No enabled embedding index found for `(label, propertyName)` | -| `422 Unprocessable Entity` | Multiple indexes match and `similarityFunction` was not specified | -| `422 Unprocessable Entity` | `query` text supplied for an external index (server cannot embed it) | -| `422 Unprocessable Entity` | `queryVector` length does not match index `dimensions` | -| `503 Service Unavailable` | Embedding model unavailable (managed indexes only) | diff --git a/docs/docs/python-sdk/ai/write-with-vectors.md b/docs/docs/python-sdk/ai/write-with-vectors.md deleted file mode 100644 index c9ac5f3f..00000000 --- a/docs/docs/python-sdk/ai/write-with-vectors.md +++ /dev/null @@ -1,247 +0,0 @@ ---- -sidebar_position: 4 -title: Writing Records with Vectors ---- - -# Writing Records with Vectors - -RushDB lets you attach pre-computed embedding vectors to records **at write time**, eliminating the need for a separate `db.ai.indexes.upsert_vectors()` call. Any operation that creates or modifies records supports this through the `vectors` parameter. - -This feature requires at least one [external index](./advanced-indexing.md) to exist for the target `(label, propertyName)`. - ---- - -## `vectors` parameter format - -All write methods accept a `vectors` list of dicts: - -```python -vectors = [ - { - "propertyName": "description", # required - "vector": [0.1, 0.9, 0.4, ...], # required - "similarityFunction": "cosine", # required only when multiple indexes share (label, propertyName) - } -] -``` - ---- - -## Create a Record with Vectors - -`db.records.create()` - -```python -record = db.records.create( - label="Article", - data={ - "title": "How transformers work", - "body": "Attention is all you need ...", - }, - vectors=[ - {"propertyName": "body", "vector": my_embedder.embed("Attention is all you need ...")} - ], -) - -print(record.id) # record created AND vector written atomically -``` - ---- - -## Upsert with Vectors - -`db.records.upsert()` - -`upsert` is idempotent on the record's natural key (`mergeBy`). Passing `vectors` writes or replaces the stored vector for each `propertyName` in the same call: - -```python -# First call — creates the record + writes vector -r1 = db.records.upsert( - label="Article", - data={"slug": "transformers-101", "title": "Transformers 101", "body": "..."}, - options={"mergeBy": ["slug"], "mergeStrategy": "append"}, - vectors=[{"propertyName": "body", "vector": v1}], -) - -# Second call — same slug → updates data + replaces the vector -r2 = db.records.upsert( - label="Article", - data={"slug": "transformers-101", "title": "Transformers 101 (revised)", "body": "Updated ..."}, - options={"mergeBy": ["slug"], "mergeStrategy": "append"}, - vectors=[{"propertyName": "body", "vector": v2}], -) - -# r1.__id == r2.__id — same record -``` - ---- - -## Set with Vectors - -`db.records.set()` - -`set` replaces all properties of a record with new values. Including `vectors` writes those vectors at the same time: - -```python -# Full replace — data AND vector updated together -db.records.set( - target=record, - label="Product", - data={"name": "Widget Pro", "price": 19.99}, - vectors=[{"propertyName": "description", "vector": new_vec}], -) -``` - ---- - -## Create Multiple Records with Vectors - -`db.records.create_many()` - -`create_many` is optimised for flat rows. Use the top-level `vectors` parameter — a list indexed by row position — to attach a vector to each record without nesting inside your flat data: - -```python -db.records.create_many( - label="Product", - data=[ - {"name": "Alpha", "description": "First product"}, - {"name": "Beta", "description": "Second product"}, - {"name": "Gamma", "description": "Third product"}, - ], - vectors=[ - [{"propertyName": "description", "vector": [1, 0, 0]}], # row 0 - [{"propertyName": "description", "vector": [0, 1, 0]}], # row 1 - [{"propertyName": "description", "vector": [0, 0, 1]}], # row 2 - ], -) -``` - -### Sparse vectors - -Leave rows without vectors by providing a shorter `vectors` list — any unspecified trailing rows are skipped: - -```python -db.records.create_many( - label="Product", - data=[{"name": "Alpha"}, {"name": "Beta"}, {"name": "Gamma"}], - # only row 0 gets a vector; rows 1 and 2 are skipped - vectors=[[{"propertyName": "description", "vector": my_vec}]], -) -``` - ---- - -## Import CSV with Vectors - -`db.records.import_csv()` - -CSV data is a raw string, so per-row vectors are supplied as a separate `vectors` parameter using the same indexed-list format. Row indices are 0-based and refer to data rows after the header is consumed: - -```python -csv_data = """name,description -Alpha,First product -Beta,Second product -Gamma,Third product""" - -db.records.import_csv( - label="Product", - data=csv_data, - vectors=[ - [{"propertyName": "description", "vector": [1, 0, 0]}], # csv row 0 - [{"propertyName": "description", "vector": [0, 1, 0]}], # csv row 1 - [{"propertyName": "description", "vector": [0, 0, 1]}], # csv row 2 - ], -) -``` - -The server returns `400 Bad Request` if `vectors` length exceeds the number of CSV data rows (validated after CSV parsing). - ---- - -## Specifying `similarityFunction` for disambiguation - -When a `(label, propertyName)` has multiple external indexes registered (e.g. one cosine and one euclidean), include `similarityFunction` in each vector entry so the server routes the write to the correct index: - -```python -# Write to the cosine index -db.records.create( - label="Product", - data={"name": "Widget"}, - vectors=[ - {"propertyName": "embedding", "vector": vec, "similarityFunction": "cosine"} - ], -) -``` - -Omitting `similarityFunction` when multiple indexes match returns `422 Unprocessable Entity`. - ---- - -## Multiple vectors in one call - -Write vectors for multiple properties or indexes in a single operation: - -```python -db.records.create( - label="Document", - data={"title": "Multi-modal doc", "abstract": "...", "fullText": "..."}, - vectors=[ - {"propertyName": "abstract", "vector": abstract_vec}, - {"propertyName": "fullText", "vector": full_text_vec}, - ], -) -``` - -Each entry is matched independently against the available external indexes. - ---- - -## Complete worked example - -```python -from rushdb import RushDB - -db = RushDB("your-api-key") -emb = YourEmbeddingModel() - -# 1. Create an external index (safe to call multiple times — 409 on duplicate) -try: - idx_response = db.ai.indexes.create({ - "label": "Article", - "propertyName": "body", - "sourceType": "external", - "dimensions": 768, - "similarityFunction": "cosine", - }) - index_id = idx_response.data["id"] -except Exception: - index_id = next( - i["id"] for i in db.ai.indexes.find().data - if i["label"] == "Article" and i["propertyName"] == "body" - ) - -# 2. Create records from your pipeline, embedding as you go -docs = [ - {"title": "Alpha", "body": "First doc"}, - {"title": "Beta", "body": "Second doc"}, -] - -for doc in docs: - db.records.create( - label="Article", - data=doc, - vectors=[{"propertyName": "body", "vector": emb.embed(doc["body"])}], - ) - -# 3. Search -query_vec = emb.embed("first document") -results = db.ai.search({ - "labels": ["Article"], - "propertyName": "body", - "queryVector": query_vec, - "limit": 3, -}) - -for r in results.data: - print(f"[{r.score:.3f}] {r['title']}") -``` diff --git a/docs/docs/python-sdk/introduction.md b/docs/docs/python-sdk/introduction.md deleted file mode 100644 index f121bbce..00000000 --- a/docs/docs/python-sdk/introduction.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Introduction -sidebar_position: 0 ---- - -# Python SDK - -Push JSON, query by value or meaning, traverse graphs — from Python. - -## Install - -```bash -pip install rushdb -``` - -## Connect - -```python -from rushdb import RushDB - -db = RushDB("RUSHDB_API_KEY") -``` - -Get your API token from the [RushDB Dashboard](https://app.rushdb.com/). - -## First write - -```python -# Nested objects become linked records automatically -db.records.create_many( - label="MOVIE", - data={ - "title": "Inception", - "rating": 8.8, - "genre": "sci-fi", - "ACTOR": [ - {"name": "Leonardo DiCaprio", "country": "USA"}, - {"name": "Ken Watanabe", "country": "Japan"} - ] - } -) -# Created: MOVIE → ACTOR × 2 (relationships wired automatically) -``` - -## First read - -```python -result = db.records.find({ - "labels": ["MOVIE"], - "where": {"rating": {"$gte": 8}}, - "limit": 10 -}) - -for movie in result: - print(movie["title"]) - -print(f"{result.total} total") -``` - -## Configuration - -| Parameter | Default | Description | -|---|---|---| -| `api_key` | — | Your RushDB API token (required) | -| `url` | `https://app.rushdb.com` | RushDB instance URL | - -```python -# Self-hosted instance -db = RushDB("RUSHDB_API_KEY", url="https://your-rushdb-instance.com") -``` - -## Namespaces - -| Namespace | Purpose | -|---|---| -| `db.records` | Create, read, update, delete records | -| `db.relationships` | Attach / detach record links | -| `db.labels` | List labels in the database | -| `db.properties` | Inspect property metadata | -| `db.transactions` | Begin / commit / rollback | -| `db.ai` | Ontology, embedding indexes, semantic search | - - diff --git a/docs/docs/python-sdk/labels.md b/docs/docs/python-sdk/labels.md deleted file mode 100644 index 89e2e998..00000000 --- a/docs/docs/python-sdk/labels.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Labels - -```python -# All labels and their record counts -result = db.labels.find({}) -# → [LabelResult(name='MOVIE', count=3), LabelResult(name='ACTOR', count=3), ...] - -# Labels that have records matching a condition -result = db.labels.find({"where": {"rating": {"$gte": 8}}}) -# → [LabelResult(name='MOVIE', count=1)] -``` - -`db.labels.find()` accepts a standard [SearchQuery](../concepts/search/where) `where` clause to filter labels by the fields of their records. It returns all labels that match regardless of how many records satisfy the condition. - - - diff --git a/docs/docs/python-sdk/properties.md b/docs/docs/python-sdk/properties.md deleted file mode 100644 index f46be773..00000000 --- a/docs/docs/python-sdk/properties.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Properties - -## Find Properties - -`db.properties.find()` - -```python -# All properties -props = db.properties.find() - -# Filtered -props = db.properties.find({ - "where": {"type": "string"}, - "limit": 20 -}) -``` - -## Find by ID - -`db.properties.find_by_id()` - -```python -prop = db.properties.find_by_id("prop-123") -``` - -## Get Property Values - -`db.properties.values()` - -Returns distinct values for a property — useful for building filter UIs or feeding into `db.ai.get_ontology()`. - -```python -values_data = db.properties.values( - property_id="prop-123", - search_query={ - "query": "sci", # filter values containing this text - "orderBy": "asc", - "limit": 100 - } -) - -print(values_data.get("values")) # list of values -print(values_data.get("min")) # numeric min -print(values_data.get("max")) # numeric max -``` - -## Delete Property - -`db.properties.delete()` - -Deletes a property and removes it from **all records** that have it. - -```python -db.properties.delete("prop-123") -``` - -:::note -Deleting a property removes its data from every record in the database, not just one record. -::: diff --git a/docs/docs/python-sdk/python-reference/_category_.json b/docs/docs/python-sdk/python-reference/_category_.json deleted file mode 100644 index 17057f9d..00000000 --- a/docs/docs/python-sdk/python-reference/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Python Reference", - "position": 9, - "collapsed": true, - "collapsible": true -} diff --git a/docs/docs/python-sdk/python-reference/transaction.md b/docs/docs/python-sdk/python-reference/transaction.md deleted file mode 100644 index a9dd875a..00000000 --- a/docs/docs/python-sdk/python-reference/transaction.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Transaction - -The `Transaction` class represents a record in RushDB and provides methods for manipulating individual records, including updates, relationships, and deletions. - -## Class Definition - -```python -class Transaction: - """Represents a RushDB transaction.""" - def __init__(self, client: "RushDB", transaction_id: str): -``` - -## Properties - -### id - -Gets the record's unique identifier. - -**Type:** `str` - -**Example:** -```python -record = client.records.create("USER", {"name": "John"}) -print(record.id) # e.g., "1234abcd-5678-..." -``` - -### proptypes - -Gets the record's property types. - -**Type:** `str` - -**Example:** -```python -record = client.records.create("USER", {"name": "John", "age": 25}) -print(record.proptypes) # Returns property type definitions -``` - -### label - -Gets the record's label. - -**Type:** `str` - -**Example:** -```python -record = client.records.create("USER", {"name": "John"}) -print(record.label) # "USER" -``` - -### timestamp - -Gets the record's creation timestamp from its ID. - -**Type:** `int` - -**Example:** -```python -record = client.records.create("USER", {"name": "John"}) -print(record.timestamp) # Unix timestamp in milliseconds -``` - -### date - -Gets the record's creation date. - -**Type:** `datetime` - -**Example:** -```python -record = client.records.create("USER", {"name": "John"}) -print(record.date) # datetime object -``` - -## Methods - -### set() - -Updates all data for the record. - -**Signature:** -```python -def set( - self, - data: Dict[str, Any], - transaction: Optional[Transaction] = None -) -> Dict[str, str] -``` - -**Arguments:** -- `data` (Dict[str, Any]): New record data -- `transaction` (Optional[Transaction]): Optional transaction object - -**Returns:** -- `Dict[str, str]`: Response data - -**Example:** -```python -record = client.records.create("USER", {"name": "John"}) -response = record.set({ - "name": "John Doe", - "email": "john@example.com", - "age": 30 -}) -``` - -### update() - -Updates specific fields of the record. - -**Signature:** -```python -def update( - self, - data: Dict[str, Any], - transaction: Optional[Transaction] = None -) -> Dict[str, str] -``` - -**Arguments:** -- `data` (Dict[str, Any]): Partial record data to update -- `transaction` (Optional[Transaction]): Optional transaction object - -**Returns:** -- `Dict[str, str]`: Response data - -**Example:** -```python -record = client.records.create("USER", { - "name": "John", - "email": "john@example.com" -}) -response = record.update({ - "email": "john.doe@example.com" -}) -``` - -### attach() - -Creates relationships with other records. - -**Signature:** -```python -def attach( - self, - target: Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], "Record", List["Record"]], - options: Optional[RelationshipOptions] = None, - transaction: Optional[Transaction] = None -) -> Dict[str, str] -``` - -**Arguments:** -- `target` (Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], Record, List[Record]]): Target record(s) -- `options` (Optional[RelationshipOptions]): Relationship options - - `direction` (Optional[Literal["in", "out"]]): Relationship direction - - `type` (Optional[str]): Relationship type -- `transaction` (Optional[Transaction]): Optional transaction object - -**Returns:** -- `Dict[str, str]`: Response data - -**Example:** -```python -# Create two records -user = client.records.create("USER", {"name": "John"}) -group = client.records.create("GROUP", {"name": "Admins"}) - -# Attach user to group -response = user.attach( - target=group, - options=RelationshipOptions( - type="BELONGS_TO", - direction="out" - ) -) -``` - -### detach() - -Removes relationships with other records. - -**Signature:** -```python -def detach( - self, - target: Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], "Record", List["Record"]], - options: Optional[RelationshipDetachOptions] = None, - transaction: Optional[Transaction] = None -) -> Dict[str, str] -``` - -**Arguments:** -- `target` (Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], Record, List[Record]]): Target record(s) -- `options` (Optional[RelationshipDetachOptions]): Detach options - - `direction` (Optional[Literal["in", "out"]]): Relationship direction - - `typeOrTypes` (Optional[Union[str, List[str]]]): Relationship type(s) -- `transaction` (Optional[Transaction]): Optional transaction object - -**Returns:** -- `Dict[str, str]`: Response data - -**Example:** -```python -# Detach user from group -response = user.detach( - target=group, - options=RelationshipDetachOptions( - typeOrTypes="BELONGS_TO", - direction="out" - ) -) -``` - -### delete() - -Deletes the record. - -**Signature:** -```python -def delete( - self, - transaction: Optional[Transaction] = None -) -> Dict[str, str] -``` - -**Arguments:** -- `transaction` (Optional[Transaction]): Optional transaction object - -**Returns:** -- `Dict[str, str]`: Response data - -**Example:** -```python -user = client.records.create("USER", {"name": "John"}) -response = user.delete() -``` - -## Complete Usage Example - -Here's a comprehensive example demonstrating various Record operations: - -```python -# Create a new record -user = client.records.create("USER", { - "name": "John Doe", - "email": "john@example.com", - "age": 30 -}) - -# Access properties -print(f"Record ID: {user.id}") -print(f"Label: {user.label}") -print(f"Created at: {user.date}") - -# Update record data -user.update({ - "age": 31, - "title": "Senior Developer" -}) - -# Create related records -department = client.records.create("DEPARTMENT", { - "name": "Engineering" -}) - -project = client.records.create("PROJECT", { - "name": "Secret Project" -}) - -# Create relationships -user.attach( - target=department, - options=RelationshipOptions( - type="BELONGS_TO", - direction="out" - ) -) - -user.attach( - target=project, - options=RelationshipOptions( - type="WORKS_ON", - direction="out" - ) -) - -# Remove relationship -user.detach( - target=project, - options=RelationshipDetachOptions( - typeOrTypes="WORKS_ON", - direction="out" - ) -) - -# Delete record -user.delete() -``` - -## Working with Transactions - -Records can be manipulated within transactions for atomic operations: - -```python -# Start a transaction -with client.transactions.begin() as transaction: - # Create user - user = client.records.create( - "USER", - {"name": "John Doe"}, - transaction=transaction - ) - - # Update user - user.update( - {"status": "active"}, - transaction=transaction - ) - - # Create and attach department - dept = client.records.create( - "DEPARTMENT", - {"name": "Engineering"}, - transaction=transaction - ) - - user.attach( - target=dept, - options=RelationshipOptions(type="BELONGS_TO"), - transaction=transaction - ) - - # Transaction will automatically commit if no errors occur - # If an error occurs, it will automatically rollback -``` diff --git a/docs/docs/python-sdk/raw-queries.md b/docs/docs/python-sdk/raw-queries.md deleted file mode 100644 index 9f75a7e2..00000000 --- a/docs/docs/python-sdk/raw-queries.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -sidebar_position: 8 ---- - -# Raw Queries - -:::warning Requires a connected Neo4j instance -This endpoint is only available when your project is connected to your own Neo4j database. Connecting a custom Neo4j instance is available on the free tier — see the RushDB dashboard to set it up. -::: - -### Python SDK example - -```py -from rushdb import RushDB - -db = RushDB("RUSHDB_API_KEY") - -result = db.query.raw({ - "query": "MATCH (n:Person) RETURN n LIMIT $limit", - "params": {"limit": 10} -}) - -print(result.get("data")) -``` - -### Real-world example: employees at a company - -```py -company = 'Acme Corp' -result = db.query.raw({ - "query": "MATCH (c:Company { name: $company })<-[:EMPLOYS]-(p:Person) RETURN p { .name, .email, company: c.name } AS employee ORDER BY p.name LIMIT $limit", - "params": {"company": company, "limit": 50} -}) - -print(result.get('data')) -``` diff --git a/docs/docs/python-sdk/records/_category_.json b/docs/docs/python-sdk/records/_category_.json deleted file mode 100644 index 530e80f3..00000000 --- a/docs/docs/python-sdk/records/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Records", - "position": 2, - "collapsible": false -} diff --git a/docs/docs/python-sdk/records/create-records.md b/docs/docs/python-sdk/records/create-records.md deleted file mode 100644 index 0b44c151..00000000 --- a/docs/docs/python-sdk/records/create-records.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create Records - -Three methods for writing records. For nested/graph data see [Import Data](./import-data.md). - -## Create a Record - -`db.records.create()` - -```python -movie = db.records.create( - label="MOVIE", - data={"title": "Inception", "rating": 8.8, "genre": "sci-fi"} -) -# → Record { __id, __label, title, rating, genre } -``` - -## Create Multiple Records - -`db.records.create_many()` - -Flat rows only — no nested objects. For nested data use [`import_json`](./import-data.md). - -```python -result = db.records.create_many( - label="ACTOR", - data=[ - {"name": "Leonardo DiCaprio", "country": "USA"}, - {"name": "Ken Watanabe", "country": "Japan"} - ] -) -# → SearchResult { data: [...], total: 2 } -``` - -## Upsert - -`db.records.upsert()` - -Create-or-update based on matching criteria. - -```python -# Match on 'title'; update rating if found, create if not -movie = db.records.upsert( - label="MOVIE", - data={"title": "Inception", "rating": 9.0, "genre": "sci-fi"}, - options={"mergeBy": ["title"], "mergeStrategy": "append"} -) -``` - -### Merge strategies - -| Strategy | Behaviour | -| ------------------ | --------------------------------------------------------------------- | -| `append` (default) | Add / update incoming fields; preserve all other existing fields | -| `rewrite` | Replace all fields with incoming data; unmentioned fields are removed | - -### `mergeBy` behaviour - -| `mergeBy` value | Match behaviour | -| --------------- | ----------------------------------- | -| `['field']` | Match only on listed fields | -| `[]` or omitted | Match on ALL incoming property keys | - -## Options - -| Option | Default | Description | -| ------------------------------- | ------------------------------- | ------------------------------------- | -| `suggestTypes` | `True` | Infer property types automatically | -| `convertNumericValuesToNumbers` | `False` | Convert string numbers to number type | -| `capitalizeLabels` | `False` | Uppercase all inferred label names | -| `relationshipType` | `__RUSHDB__RELATION__DEFAULT__` | Relationship type for nested links | -| `returnResult` | `True` | Return created records in response | -| `mergeBy` | — | Fields to match on for upsert | -| `mergeStrategy` | `append` | `append` or `rewrite` | - -## With a transaction - -```python -tx = db.tx.begin() -try: - movie = db.records.create( - label="MOVIE", - data={"title": "Inception"}, - transaction=tx - ) - actor = db.records.create( - label="ACTOR", - data={"name": "Leonardo DiCaprio"}, - transaction=tx - ) - db.relationships.attach( - source=movie, - target=actor, - options={"type": "STARS"}, - transaction=tx - ) - tx.commit() -except Exception: - tx.rollback() - raise -``` diff --git a/docs/docs/python-sdk/records/delete-records.md b/docs/docs/python-sdk/records/delete-records.md deleted file mode 100644 index 1147e94e..00000000 --- a/docs/docs/python-sdk/records/delete-records.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Delete Records - -## Delete by ID - -`db.records.delete_by_id()` - -```python -# Single record -db.records.delete_by_id("movie-123") - -# Multiple records -db.records.delete_by_id(["movie-123", "movie-456"]) - -# From record object -movie.delete() -``` - -## Bulk Delete - -`db.records.delete()` - -Delete all records matching a query. - -```python -db.records.delete({ - "labels": ["MOVIE"], - "where": {"rating": {"$lt": 5}} -}) -``` - -:::warning -Calling `delete()` without a `where` clause deletes **all** records with the given label. -::: - -## With a transaction - -```python -tx = db.tx.begin() -try: - db.records.delete_by_id("movie-123", transaction=tx) - tx.commit() -except Exception: - tx.rollback() - raise -``` diff --git a/docs/docs/python-sdk/records/get-records.md b/docs/docs/python-sdk/records/get-records.md deleted file mode 100644 index e2428d52..00000000 --- a/docs/docs/python-sdk/records/get-records.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Get Records - -```python -result = db.records.find({ - "labels": ["MOVIE"], - "where": {"rating": {"$gte": 8}}, - "limit": 10, - "orderBy": {"rating": "desc"} -}) - -for movie in result: - print(movie.get("title"), movie.get("rating")) - -print(f"{len(result)} shown, {result.total} total") -``` - -## Find by ID - -`db.records.find_by_id()` - -```python -# Single record -movie = db.records.find_by_id("movie-123") - -# Multiple records -movies = db.records.find_by_id(["movie-123", "movie-456"]) -``` - -## SearchQuery parameters - -| Parameter | Type | Description | -| --------- | ----------- | --------------------------------------------------- | -| `labels` | `list[str]` | Filter by one or more labels | -| `where` | `dict` | Field conditions and operators | -| `orderBy` | `dict` | `{"field": "asc" \| "desc"}` | -| `limit` | `int` | Max records to return. **Omit when using `select`** | -| `skip` | `int` | Records to skip (pagination offset) | -| `select` | `dict` | Output-shaping expressions (preferred) | -| `groupBy` | `list[str]` | Group results (with select) | - -## Relationship traversal - -```python -# MOVIE that has ACTOR named DiCaprio -result = db.records.find({ - "labels": ["MOVIE"], - "where": { - "ACTOR": { - "name": {"$contains": "DiCaprio"} - } - } -}) - -# With explicit relationship type and direction -result = db.records.find({ - "labels": ["MOVIE"], - "where": { - "ACTOR": { - "$relation": {"type": "STARS_IN", "direction": "in"}, - "country": "USA" - } - } -}) -``` - -## Where operators - -```python -# Common operators -{"rating": {"$gt": 7, "$lt": 10}} # gt, gte, lt, lte -{"genre": {"$in": ["sci-fi", "drama"]}} # in, nin -{"title": {"$contains": "Inc"}} # contains, startsWith, endsWith -{"sequel": {"$exists": False}} # exists, not exists -{"id": {"$id": "movie-123"}} # match by __id -``` - -## Select Expressions - -```python -result = db.records.find({ - "labels": ["MOVIE"], - "select": { - "count": {"$count": "*"}, - "avgRating": {"$avg": "$record.rating"}, - "titles": {"$collect": {"from": "$record", "select": {"title": "$record.title"}}} - } - # Do NOT add "limit" when using select -}) -``` - -Expressions: `$count` · `$sum` · `$avg` · `$min` · `$max` · `$collect` · `$timeBucket` · `$ref` · `$add` · `$subtract` · `$multiply` · `$divide` - -:::danger -**Never set `limit` with `select`** — it restricts the record scan and produces wrong totals. -::: - -## GroupBy - -```python -result = db.records.find({ - "labels": ["MOVIE"], - "select": { - "count": {"$count": "*"}, - "avgRating": {"$avg": "$record.rating"} - }, - "groupBy": ["$record.genre"], - "orderBy": {"count": "desc"} # late-ordering: ensures correct totals -}) -``` - -## TimeBucket (time-series) - -```python -result = db.records.find({ - "labels": ["MOVIE"], - "select": { - "month": {"$timeBucket": {"field": "$record.releasedAt", "unit": "month"}}, - "count": {"$count": "*"} - }, - "groupBy": ["month"], - "orderBy": {"month": "asc"} -}) -``` - -Unit values: `"day"` · `"week"` · `"month"` · `"quarter"` · `"year"` · `"hours"` · `"minutes"` · `"seconds"` (use plural forms with `"size"` for custom windows). - -## Collect related records - -Label-based (no alias needed — preferred for nesting): - -```python -result = db.records.find({ - "labels": ["COMPANY"], - "select": { - "departments": { - "$collect": { - "label": "DEPARTMENT", - "select": { - "name": "$self.name", - "projects": { - "$collect": { - "label": "PROJECT", - "select": {"name": "$self.name"} - } - } - } - } - } - } -}) -``` - -Alias-based (requires `$alias` in `where`): - -```python -result = db.records.find({ - "labels": ["MOVIE"], - "where": { - "ACTOR": {"$alias": "$actor"} - }, - "select": { - "actors": { - "$collect": { - "from": "$actor", - "select": {"name": "$actor.name", "country": "$actor.country"} - } - } - } -}) -``` - -## SearchResult - -```python -result = db.records.find({"labels": ["MOVIE"], "limit": 10}) - -len(result) # records returned in this page (≤ limit) -result.total # total records matching in the database -result.has_more # True if more pages remain -result[0] # access by index -for r in result: # iterable - pass -``` - -## With a transaction - -```python -tx = db.tx.begin() -try: - result = db.records.find({"labels": ["MOVIE"]}, transaction=tx) - tx.commit() -except Exception: - tx.rollback() - raise -``` diff --git a/docs/docs/python-sdk/records/import-data.md b/docs/docs/python-sdk/records/import-data.md deleted file mode 100644 index 9dda42d7..00000000 --- a/docs/docs/python-sdk/records/import-data.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Import Data - -Pass nested dicts — RushDB walks the structure and links each level as a related record. - -```python -db.records.create_many( - label="MOVIE", - data={ - "title": "Inception", - "rating": 8.8, - "ACTOR": [ - {"name": "Leonardo DiCaprio", "country": "USA"}, - {"name": "Ken Watanabe", "country": "Japan"} - ] - } -) -# MOVIE → ACTOR × 2: all created and linked automatically -``` - -## Flat batch (`create_many`) - -Use `create_many` with a list for flat rows — no nested objects inside items. - -```python -db.records.create_many( - label="ACTOR", - data=[ - {"name": "Leonardo DiCaprio", "country": "USA"}, - {"name": "Ken Watanabe", "country": "Japan"} - ], - options={"suggestTypes": True} -) -``` - -## CSV import (`import_csv`) - -```python -with open("actors.csv") as f: - csv_content = f.read() - -db.records.import_csv( - label="ACTOR", - data=csv_content, - options={"returnResult": False}, - parse_config={"header": True, "dynamicTyping": True} -) -``` - -### `parse_config` options - -| Option | Default | Description | -|---|---|---| -| `delimiter` | `,` | Column separator | -| `header` | `True` | First row is header | -| `skipEmptyLines` | `True` | Ignore blank rows | -| `dynamicTyping` | `True` | Auto-convert numbers and booleans | -| `quoteChar` | `"` | Quote character | -| `escapeChar` | `"` | Escape character | -| `newline` | auto | Explicit newline sequence | - -## Options - -| Option | Default | Description | -|---|---|---| -| `suggestTypes` | `True` | Infer property types automatically | -| `convertNumericValuesToNumbers` | `False` | Convert string numbers to number type | -| `capitalizeLabels` | `False` | Uppercase all inferred label names | -| `relationshipType` | `__RUSHDB__RELATION__DEFAULT__` | Relationship type for nested links | -| `returnResult` | `True` | Return created records in response | -| `mergeBy` | — | Fields to match on for upsert | -| `mergeStrategy` | `append` | `append` or `rewrite` | - -## Upsert during import - -```python -# Append — update matched, preserve other fields -db.records.create_many( - label="ACTOR", - data=actors, - options={"mergeBy": ["name"], "mergeStrategy": "append"} -) - -# Rewrite — replace all properties for matched records -db.records.import_csv( - label="ACTOR", - data=csv_content, - options={"mergeBy": ["name"], "mergeStrategy": "rewrite"} -) -``` - -## Quick rules recap - -- `create_many` with a dict or list of dicts: flat or nested JSON -- `import_csv`: CSV string input with `parse_config`; `dynamicTyping` inherits from `options.suggestTypes` when omitted -- Set `returnResult: False` for large imports to improve performance - - diff --git a/docs/docs/python-sdk/records/update-records.md b/docs/docs/python-sdk/records/update-records.md deleted file mode 100644 index bba6183c..00000000 --- a/docs/docs/python-sdk/records/update-records.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Update Records - -## Partial Update - -`db.records.update()` - -Unspecified fields are preserved. - -```python -# Update via record object -movie.update({"rating": 9.0}) - -# Update by ID -db.records.update( - record_id=movie.__id, - data={"rating": 9.0} -) -``` - -## Full Replacement - -`db.records.set()` - -All previous fields are removed, then replaced with the new data. - -```python -# Set via record object -movie.set({"title": "Inception", "rating": 9.0, "genre": "sci-fi"}) - -# Set by ID -db.records.set( - record_id=movie.__id, - data={"title": "Inception", "rating": 9.0, "genre": "sci-fi"} -) -``` - -## Parameters - -| Parameter | Type | Description | -| ------------- | ------------- | -------------------------- | -| `record_id` | `str` | ID of the record to update | -| `data` | `dict` | Properties to write | -| `transaction` | `Transaction` | Optional transaction | - -## With a transaction - -```python -tx = db.tx.begin() -try: - db.records.update(record_id=movie.__id, data={"rating": 9.0}, transaction=tx) - tx.commit() -except Exception: - tx.rollback() - raise -``` diff --git a/docs/docs/python-sdk/relationships.md b/docs/docs/python-sdk/relationships.md deleted file mode 100644 index ce184d1a..00000000 --- a/docs/docs/python-sdk/relationships.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Relationships - -```python -# Leo acted in Inception -db.records.attach( - source=leo, - target=inception, - options={"type": "ACTED_IN"} -) - -# Detach -db.records.detach( - source=leo, - target=inception, - options={"type": "ACTED_IN"} -) -``` - -## Attach - -`attach()` - -```python -# With direction -db.records.attach( - source=movie, - target=actor, - options={"type": "STARS_IN", "direction": "out"} -) - -# One-to-many (target list) -db.records.attach( - source=movie, - target=[actor1, actor2, actor3], - options={"type": "STARS_IN"} -) -``` - -## Detach - -`detach()` - -```python -db.records.detach( - source=movie, - target=actor, - options={"type": "STARS_IN"} -) -``` - -## Direction - -| Value | Meaning | -| ------- | --------------- | -| `"out"` | source → target | -| `"in"` | target → source | - -## With a transaction - -```python -tx = db.tx.begin() -try: - db.records.attach(source=movie, target=actor, options={"type": "STARS_IN"}, transaction=tx) - tx.commit() -except Exception: - tx.rollback() - raise -``` - -For traversal in queries, see [Get Records — Relationship traversal](./records/get-records.md#relationship-traversal). diff --git a/docs/docs/python-sdk/transactions.md b/docs/docs/python-sdk/transactions.md deleted file mode 100644 index fcb75b6d..00000000 --- a/docs/docs/python-sdk/transactions.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Transactions - -Group writes atomically — all succeed or all roll back. - -## Context manager (idiomatic) - -```python -# Auto-commit on success, auto-rollback on exception -with db.tx.begin() as tx: - leo = db.records.create(label="ACTOR", data={"name": "Leonardo DiCaprio"}, transaction=tx) - inception = db.records.create(label="MOVIE", data={"title": "Inception"}, transaction=tx) - db.records.attach(source=leo, target=inception, options={"type": "ACTED_IN"}, transaction=tx) -# committed automatically — no explicit commit() call needed -``` - -## Manual commit / rollback - -```python -tx = db.tx.begin() -try: - movie = db.records.create(label="MOVIE", data={"title": "Inception"}, transaction=tx) - actor = db.records.create(label="ACTOR", data={"name": "Leonardo DiCaprio"}, transaction=tx) - db.records.attach(source=movie, target=actor, options={"type": "STARS_IN"}, transaction=tx) - tx.commit() -except Exception: - tx.rollback() - raise -``` - -## API - -| Method | Description | -| ------------------- | ----------------------- | -| `db.tx.begin(ttl?)` | Start a new transaction | -| `tx.commit()` | Persist all operations | -| `tx.rollback()` | Discard all operations | - -## Timeouts - -| Setting | Value | -| ----------- | -------- | -| Default TTL | 5000 ms | -| Maximum TTL | 30000 ms | - -```python -tx = db.tx.begin(ttl=15000) # 15 s timeout -``` - -## Supported operations - -`create` · `create_many` · `update` · `set` · `delete` · `delete_by_id` · `attach` · `detach` · `find` diff --git a/docs/docs/rest-api/_category_.json b/docs/docs/rest-api/_category_.json deleted file mode 100644 index 79a6a768..00000000 --- a/docs/docs/rest-api/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "REST API", - "position": 12, - "collapsed": true, - "collapsible": true -} diff --git a/docs/docs/rest-api/ai/_category_.json b/docs/docs/rest-api/ai/_category_.json deleted file mode 100644 index 845c101e..00000000 --- a/docs/docs/rest-api/ai/_category_.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "label": "AI & Vectors", - "position": 1, - "collapsed": false, - "collapsible": true, - "link": { - "type": "doc", - "id": "rest-api/ai/overview" - } -} diff --git a/docs/docs/rest-api/introduction.md b/docs/docs/rest-api/introduction.md deleted file mode 100644 index 2645c813..00000000 --- a/docs/docs/rest-api/introduction.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Introduction -sidebar_position: 0 ---- - -# REST API - -Base URL: `https://api.rushdb.com/api/v1` -Auth: `Authorization: Bearer YOUR_TOKEN` - -```bash -curl -X POST https://api.rushdb.com/api/v1/records \ - -H "Authorization: Bearer $RUSHDB_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"label":"MOVIE","data":{"title":"Inception","rating":8.8}}' -``` - -Interactive docs: [Swagger UI](https://api.rushdb.com/api) · [OpenAPI JSON](https://api.rushdb.com/api-json) - -## Endpoints - -| Group | Description | -|---|---| -| [Records](./records/create-records) | Create, read, update, delete, search, import, export | -| [Relationships](./relationships) | Attach and detach edges between records | -| [Labels](./labels) | Query which types exist and their counts | -| [Properties](./properties) | Inspect field names, types, and value ranges | -| [Transactions](./transactions) | Atomic multi-step operations | -| [AI & Semantic Search](./ai/overview) | Schema export + vector similarity search | -| [Raw Queries](./raw-queries) | Cypher pass-through (cloud only) | - - diff --git a/docs/docs/rest-api/records/_category_.json b/docs/docs/rest-api/records/_category_.json deleted file mode 100644 index 530e80f3..00000000 --- a/docs/docs/rest-api/records/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Records", - "position": 2, - "collapsible": false -} diff --git a/docs/docs/concepts/billing-model.md b/docs/docs/rushdb-cloud/billing-model.md similarity index 53% rename from docs/docs/concepts/billing-model.md rename to docs/docs/rushdb-cloud/billing-model.md index b6546517..be6e7570 100644 --- a/docs/docs/concepts/billing-model.md +++ b/docs/docs/rushdb-cloud/billing-model.md @@ -1,12 +1,15 @@ --- -sidebar_position: 8 +slug: /cloud/billing-model +title: Billing Model +description: How RushDB Cloud bills for Knowledge Units, applies plan limits, and treats self-hosted deployments. +sidebar_position: 1 --- # Billing Model ## Overview -RushDB uses a **Knowledge Units (KU)** billing model. You pay for the structured knowledge RushDB creates and maintains from your data — not for infrastructure, nodes, edges, storage, or compute. +RushDB Cloud uses a **Knowledge Units (KU)** billing model. You pay for the structured knowledge RushDB creates and maintains from your data — not for infrastructure, nodes, edges, storage, or compute. ``` Total KU consumed in billing period × price per KU = your bill @@ -24,14 +27,14 @@ Bill = included KU (flat rate) + overage KU × overage rate ## Plan Comparison -| | Free | Pro | Scale | Enterprise | -|---|---|---|---|---| -| Included KU / month | 100,000 | 10,000,000 | Usage-based | Custom | -| Overage | Not available | Per KU | Per KU | Negotiated | -| Projects | 2 | Unlimited | Unlimited | Unlimited | -| Self-hosted support | ✓ | ✓ | ✓ | ✓ | -| SLA | — | — | ✓ | ✓ | -| BYOC (own Neo4j / Aura) | ✓ | ✓ | ✓ | ✓ | +| | Free | Pro | Scale | Enterprise | +| ----------------------- | ------------- | ---------- | ----------- | ---------- | +| Included KU / month | 100,000 | 10,000,000 | Usage-based | Custom | +| Overage | Not available | Per KU | Per KU | Negotiated | +| Projects | 2 | Unlimited | Unlimited | Unlimited | +| Self-hosted support | ✓ | ✓ | ✓ | ✓ | +| SLA | — | — | ✓ | ✓ | +| BYOC (own Neo4j / Aura) | ✓ | ✓ | ✓ | ✓ | ## One Metric @@ -45,9 +48,7 @@ KU is the only number that matters for billing. RAM, CPU, storage, node counts, ## Self-Hosted Billing -Self-hosted deployments (`RUSHDB_SELF_HOSTED=true`) have no KU limits and no billing. The OSS engine runs entirely without quota enforcement. - -For teams that need the full platform (dashboard, projects, API key management, team members) without usage caps, contact us about an **Enterprise platform license**. +Self-hosted deployments (`RUSHDB_SELF_HOSTED=true`) have no RushDB Cloud KU limits and no cloud billing. The self-hosted platform runs without quota enforcement. ## Changes to Limits Mid-Period @@ -64,3 +65,9 @@ Your KU consumption is visible in real-time on the **KU Usage** page in your wor - Estimated end-of-month cost - Daily consumption trend chart - Ingestion and query spike timeline + +## Related Pages + +- [Knowledge Units (KU)](/cloud/knowledge-units) +- [Multitenancy & Project Isolation](/cloud/project-isolation) +- [Licensing & Third-Party Services](/cloud/licensing-and-services) diff --git a/docs/docs/rushdb-cloud/index.mdx b/docs/docs/rushdb-cloud/index.mdx new file mode 100644 index 00000000..8af2ecc0 --- /dev/null +++ b/docs/docs/rushdb-cloud/index.mdx @@ -0,0 +1,33 @@ +--- +slug: /cloud/ +title: RushDB Cloud +description: Understand RushDB Cloud billing, project isolation, licensing, third-party services, and self-hosted alternatives. +sidebar_position: 1 +--- + +# RushDB Cloud + +RushDB Cloud is the managed way to run RushDB. It provides projects, API keys, team access, usage tracking, and a dashboard while preserving the same graph model and APIs used by self-hosted deployments. + +## Choose an Operating Model + +| Model | Graph backend | RushDB operations | Best fit | +| ------------------- | ------------------------------------------------ | -------------------- | ------------------------------------------------------ | +| **RushDB Cloud** | RushDB-managed Neo4j | Managed by RushDB | Fastest path from API key to production | +| **Cloud with BYOC** | Your Neo4j or Aura instance | Managed by RushDB | Keep graph data in your own cloud account | +| **Self-hosted** | Your Neo4j, PostgreSQL, or SQLite infrastructure | Managed by your team | Full deployment control and no cloud quota enforcement | + +## Cloud Reference + +| Page | What it explains | +| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| [Billing Model](/cloud/billing-model) | Monthly plans, usage limits, and the KU-based billing model | +| [Multitenancy & Project Isolation](/cloud/project-isolation) | How `{projectId}` scopes reads, writes, metadata, and BYOC connections | +| [Licensing & Third-Party Services](/cloud/licensing-and-services) | RushDB software licenses and the runtime services involved in each operating model | +| [Knowledge Units (KU)](/cloud/knowledge-units) | What generates KU and how to estimate usage | + +## Start Using RushDB Cloud + +1. [Get an API key](/deploy/get-api-key). +2. [Connect through an SDK, MCP, or REST](/connect/). +3. Use [BYOC with Neo4j or Aura](/deploy/infra-neo4j) when graph data must remain in your own cloud account. diff --git a/docs/docs/concepts/knowledge-units.md b/docs/docs/rushdb-cloud/knowledge-units.md similarity index 68% rename from docs/docs/concepts/knowledge-units.md rename to docs/docs/rushdb-cloud/knowledge-units.md index a32a99b7..850f3b50 100644 --- a/docs/docs/concepts/knowledge-units.md +++ b/docs/docs/rushdb-cloud/knowledge-units.md @@ -1,5 +1,8 @@ --- -sidebar_position: 7 +slug: /cloud/knowledge-units +title: Knowledge Units (KU) +description: Understand the RushDB Cloud usage metric for ingestion, graph structure, compute-intensive operations, and storage footprint. +sidebar_position: 4 --- # Knowledge Units (KU) @@ -28,13 +31,13 @@ RushDB handles all the underlying storage and graph mechanics automatically. You KU is generated by operations that create or maintain structured knowledge in RushDB: -| Activity | Example | -|---|---| -| Ingesting records | `db.records.create()`, `records.importJson()` | -| Building relationships | `db.records.attach()` | -| Storing embeddings (number arrays) | Passing `number[]` arrays during ingestion | -| **Compute-intensive operations** | Vector similarity search, raw Cypher execution, multi-hop traversals | -| Maintaining stored knowledge | Ongoing footprint of stored records | +| Activity | Example | +| ---------------------------------- | -------------------------------------------------------------------- | +| Ingesting records | `db.records.create()`, `records.importJson()` | +| Building relationships | `db.records.attach()` | +| Storing embeddings (number arrays) | Passing `number[]` arrays during ingestion | +| **Compute-intensive operations** | Vector similarity search, raw Cypher execution, multi-hop traversals | +| Maintaining stored knowledge | Ongoing footprint of stored records | **Standard reads and queries (find, filter, paginate) never consume KU.** Compute-intensive operations do because their cost scales with dataset size rather than the data written — running a semantic search across 10M records costs significantly more server compute than fetching a record by ID. @@ -44,11 +47,11 @@ KU is generated by operations that create or maintain structured knowledge in Ru Each plan includes a monthly KU allowance: -| Plan | Included KU / month | -|---|---| -| Free | 100,000 KU | -| Pro | 10,000,000 KU | -| Scale | Usage-based | +| Plan | Included KU / month | +| ---------- | ------------------------- | +| Free | 100,000 KU | +| Pro | 10,000,000 KU | +| Scale | Usage-based | | Enterprise | Custom / platform license | When your workspace reaches its KU limit, write operations will be blocked until the next billing period or until you upgrade your plan. @@ -65,15 +68,16 @@ When you ingest nested JSON, RushDB **automatically decomposes it** into separat // → Record: user (0 own properties) // → Record: profile (2 properties: name, age) → ~2 KU // → Relationship: user → profile → ~0.5 KU + // Total: ~2.5 KU ``` This means nesting **does not multiply properties** — it creates additional records and relationships, each billed independently. -| Component | Cost | -|---|---| -| Each record created | weight per record | -| Each property stored on a record | weight per property | +| Component | Cost | +| ---------------------------------------- | ------------------------ | +| Each record created | weight per record | +| Each property stored on a record | weight per property | | Each relationship formed between records | fractional weight (< 1×) | > **Note:** Exact per-operation weights are not publicly exposed. You interact only with your total KU consumption. @@ -88,6 +92,7 @@ estimated monthly KU ``` Where: + - `records_per_day` — average number of top-level records ingested per day - `avg_properties` — average number of scalar properties per record - `avg_child_records` — average number of nested objects per record (each becomes a separate linked child record) @@ -102,8 +107,14 @@ RushDB charges a small daily **storage footprint** KU for every record currently - Storage KU is calculated once per day, prorated against your monthly plan allowance. - The charge is proportional to your current record count — fewer records, lower footprint. - **Deleting records reduces your ongoing footprint** from the next daily cycle onward. -- KU consumed *at creation time* is never reversed by a later deletion. +- KU consumed _at creation time_ is never reversed by a later deletion. ## Self-Hosted -When running RushDB in self-hosted mode (`RUSHDB_SELF_HOSTED=true`), KU tracking is disabled and no limits apply. Self-hosted deployments are fully unlimited. +When running RushDB in self-hosted mode (`RUSHDB_SELF_HOSTED=true`), RushDB Cloud KU tracking is disabled and no cloud usage limits apply. + +## Related Pages + +- [Billing Model](/cloud/billing-model) +- [Multitenancy & Project Isolation](/cloud/project-isolation) +- [Licensing & Third-Party Services](/cloud/licensing-and-services) diff --git a/docs/docs/rushdb-cloud/licensing-and-services.mdx b/docs/docs/rushdb-cloud/licensing-and-services.mdx new file mode 100644 index 00000000..42f91392 --- /dev/null +++ b/docs/docs/rushdb-cloud/licensing-and-services.mdx @@ -0,0 +1,64 @@ +--- +slug: /cloud/licensing-and-services +title: Licensing & Third-Party Services +description: RushDB software licenses and the storage, model, identity, email, CAPTCHA, and billing services that may participate in a deployment. +sidebar_position: 3 +--- + +# Licensing & Third-Party Services + +RushDB Cloud combines RushDB software with storage infrastructure and optional integrations. The exact service boundary depends on whether you use managed cloud, BYOC, or self-hosted deployment. + +This page is an engineering reference, not legal advice. Review each applicable license and provider agreement before production use. + +## RushDB Software Licenses + +| Component | License | Notes | +| ---------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| Documentation | [Apache License 2.0](https://github.com/rush-db/rushdb/blob/main/docs/LICENSE) | Source for this documentation site | +| TypeScript / JavaScript SDK | [Apache License 2.0](https://github.com/rush-db/rushdb/blob/main/packages/javascript-sdk/LICENSE) | Client library published as `@rushdb/javascript-sdk` | +| MCP server and agent skills | Apache License 2.0 | Packages published as `@rushdb/mcp-server` and `@rushdb/skills` | +| RushDB platform core and dashboard | [Elastic License 2.0](https://github.com/rush-db/rushdb/blob/main/platform/LICENSE) | Review the ELv2 limitations before redistributing or offering the platform as a managed service | +| RushDB Cloud | Commercial managed service | Your subscribed plan and applicable service terms govern hosted usage | + +## Runtime Services + +| Service | When it participates | Configuration or reference | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| **Neo4j and APOC Core** | Required graph backend. Use the bundled deployment, an existing Neo4j instance, or Aura through BYOC. | [Neo4j & Aura](/deploy/infra-neo4j) | +| **SQLite or PostgreSQL** | Required SQL metadata store for users, workspaces, projects, tokens, OAuth sessions, and access grants. SQLite is the zero-config self-hosted default; PostgreSQL is appropriate for cloud deployments. | [PostgreSQL / SQLite](/deploy/infra-database) | +| **OpenAI-compatible embedding endpoint** | Optional managed embeddings for semantic search. The configured provider receives text selected for embedding. | `RUSHDB_EMBEDDING_BASE_URL`, `RUSHDB_EMBEDDING_MODEL` | +| **OpenAI-compatible chat completions endpoint** | Optional relationship suggestions. The configured provider receives a compact ontology containing labels, properties, sampled values, and existing relationship patterns. | `RUSHDB_LLM_BASE_URL`, `RUSHDB_LLM_MODEL` | +| **SMTP provider** | Optional confirmation, password reset, and workspace invitation email. | `MAIL_HOST`, `MAIL_USER`, `MAIL_FROM` | +| **Google and GitHub OAuth** | Optional dashboard login providers. | `GOOGLE_CLIENT_ID`, `GH_CLIENT_ID` | +| **reCAPTCHA** | Optional login and form abuse protection. | `SERVICE_CAPTCHA_KEY` | +| **External billing service** | Optional cloud billing enforcement. | `BILLING_SERVICE_URL`, `RUSHDB_BILLING_SECRET` | +| **Stripe Checkout** | Optional dashboard checkout integration. | `VITE_STRIPE_PUBLIC_KEY` | + +## Models and OpenRouter + +RushDB does not hardcode one model vendor into the graph engine. Embeddings and relationship suggestions call OpenAI-compatible endpoints configured by the deployment: + +```env +RUSHDB_EMBEDDING_BASE_URL=https://openrouter.ai/api/v1 +RUSHDB_EMBEDDING_MODEL=qwen/qwen3-embedding-8b + +RUSHDB_LLM_BASE_URL=https://api.openai.com/v1 +RUSHDB_LLM_MODEL=gpt-4.1-mini +``` + +OpenRouter is one supported endpoint option. OpenAI, Azure OpenAI, Ollama, and other compatible providers can be used as well. Provider-specific model availability, data handling, billing, and terms apply. + +Omit the embedding variables to disable managed embedding indexes. Omit the LLM variables to disable automatic relationship suggestions. External embedding indexes remain available when your application supplies vectors directly. + +## Responsibility by Operating Model + +| Concern | RushDB Cloud | Cloud with BYOC | Self-hosted | +| -------------------------------------- | ------------------- | ------------------- | -------------------------- | +| RushDB API and dashboard operations | RushDB | RushDB | Your team | +| Neo4j account and graph infrastructure | RushDB | Your team | Your team | +| SQL metadata infrastructure | RushDB | RushDB | Your team | +| Optional model endpoint selection | Cloud configuration | Cloud configuration | Your environment variables | +| Third-party provider terms | Apply where enabled | Apply where enabled | Apply where enabled | + +See [Environment Variables](/deploy/config-env-variables) for the complete self-hosted configuration surface. diff --git a/docs/docs/rushdb-cloud/project-isolation.mdx b/docs/docs/rushdb-cloud/project-isolation.mdx new file mode 100644 index 00000000..cf0dc444 --- /dev/null +++ b/docs/docs/rushdb-cloud/project-isolation.mdx @@ -0,0 +1,48 @@ +--- +slug: /cloud/project-isolation +title: Multitenancy & Project Isolation +description: How RushDB scopes graph data, metadata, API tokens, and BYOC connections with projectId. +sidebar_position: 2 +--- + +# Multitenancy & Project Isolation + +`{projectId}` is the data boundary for a RushDB project. RushDB resolves it from authenticated request context and applies it to graph operations, project metadata, usage events, and optional BYOC database connections. + +## Graph Data Is Scoped by `projectId` + +Every RushDB record carries an internal project identifier. Generated reads and writes include that identifier as a mandatory bound parameter: + +```cypher +MATCH (record:__RUSHDB__LABEL__RECORD__ { + __RUSHDB__KEY__PROJECT__ID__: $projectId +}) +``` + +RushDB property-definition nodes are scoped too. Their uniqueness constraint includes `projectId`, so two projects can independently define a property named `email` without sharing the same metadata node. + +## Authentication Establishes the Boundary + +API tokens are project-scoped. RushDB validates the presented token, loads its project assignment from SQL metadata, and injects that project ID into the request context used by graph queries. + +The dashboard passes `x-project-id` while navigating project-level screens. MCP OAuth flows carry `project_id` context when selecting a project. These values are still checked against authenticated access; applications should never treat an arbitrary client-supplied project ID as authorization. + +## Logical and Physical Isolation + +The default shared-cloud connection uses **logical isolation**: projects can share a Neo4j database while mandatory `projectId` predicates prevent cross-project reads and writes. + +When you need a dedicated graph boundary, use **BYOC** to connect a project to your own Neo4j or Aura instance. For the strongest physical separation in self-hosted environments, run separate RushDB and Neo4j instances per tenant. + +| Requirement | Recommended model | +| ----------------------------------- | -------------------------------- | +| Fastest managed setup | RushDB Cloud | +| Graph data in your cloud account | Cloud with BYOC | +| Dedicated infrastructure per tenant | Separate self-hosted deployments | + +## Operational Guidance + +- Use separate projects for development, staging, and production. +- Issue project-scoped API keys with the minimum required access level. +- Keep Neo4j Bolt ports private and route application access through RushDB. +- Review the [Security](/deploy/config-security) guide before production deployment. +- Read [Neo4j & Aura](/deploy/infra-neo4j) when configuring BYOC. diff --git a/docs/docs/tutorials/_category_.json b/docs/docs/tutorials/_category_.json deleted file mode 100644 index 37b30f8b..00000000 --- a/docs/docs/tutorials/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "Tutorials", - "collapsed": false, - "collapsible": false, - "link": { - "type": "doc", - "id": "tutorials/index" - } -} diff --git a/docs/docs/tutorials/local-setup.md b/docs/docs/tutorials/local-setup.md deleted file mode 100644 index 325095a9..00000000 --- a/docs/docs/tutorials/local-setup.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: Local Setup -description: Spin up a local RushDB instance with Docker Compose — no repository clone required. -sidebar_position: 2 -tags: [Getting Started, Deployment] ---- - -# Local Setup - -This guide will help you set up a local development environment for RushDB using Docker, without needing to clone the repository. This is ideal for developers who want to work with RushDB in a containerized environment. - -## Prerequisites - -Before starting, ensure you have the following installed: - -1. **Docker Engine**: - - For macOS: [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) - - For Windows: [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) - - For Linux: [Docker Engine](https://docs.docker.com/engine/install/) - -2. **Docker Compose** (included with Docker Desktop, but may need to be installed separately on Linux) - -3. **Recommended System Resources**: - - Minimum: 2GB RAM, 1 CPU - - Recommended: 4GB RAM, 2 CPUs - -## Option 1: Quick Setup with External Neo4j Instance - -If you already have a Neo4j instance running (either locally or in the cloud), you can quickly start RushDB connected to it using Docker. - -### Using Docker Run Command - -```bash -docker run -p 3000:3000 \ ---name rushdb \ --e NEO4J_URL='neo4j+s://your-neo4j-instance.databases.neo4j.io' \ --e NEO4J_USERNAME='neo4j' \ --e NEO4J_PASSWORD='your-password' \ -rushdb/platform -``` - -### Using Docker Compose - -Create a `docker-compose.yml` file with the following content: - -```yaml -version: '3.8' -services: - rushdb: - image: rushdb/platform - container_name: rushdb - ports: - - "3000:3000" - environment: - - NEO4J_URL=neo4j+s://your-neo4j-instance.databases.neo4j.io - - NEO4J_USERNAME=neo4j - - NEO4J_PASSWORD=your-password -``` - -Then run: - -```bash -docker-compose up -d -``` - -## Option 2: Complete Development Environment with Neo4j - -For a fully self-contained development environment with both RushDB and Neo4j: - -### Create a Development Docker Compose Setup - -1. Create a `docker-compose.yml` file with the following content: - -
-docker-compose.yml - -```yaml -version: '3.8' -services: - rushdb: - image: rushdb/platform - container_name: rushdb - depends_on: - neo4j: - condition: service_healthy - ports: - - "3000:3000" - environment: - - NEO4J_URL=bolt://neo4j - - NEO4J_USERNAME=neo4j - - NEO4J_PASSWORD=password - - SQL_DB_TYPE=sqlite # SQLite is the default; no extra service needed - neo4j: - image: neo4j:2026.01.4 - healthcheck: - test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider localhost:7474 || exit 1" ] - interval: 5s - retries: 30 - start_period: 10s - ports: - - "7474:7474" - - "7687:7687" - environment: - - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes - - NEO4J_AUTH=neo4j/password - - NEO4J_PLUGINS=["apoc"] - volumes: - - neo4j-plugins:/var/lib/neo4j/plugins - - neo4j-data:/data - - neo4j-logs:/logs - - neo4j-conf:/var/lib/neo4j/conf - -volumes: - neo4j-plugins: - neo4j-data: - neo4j-logs: - neo4j-conf: -``` -
- -:::info -The default `SQL_DB_TYPE=sqlite` stores user/workspace/project data in a local `rushdb.db` file inside the container. For persistent storage across container restarts, either mount a volume to the path specified by `SQL_DB_PATH` or switch to an external PostgreSQL instance. -::: - -2. Start the environment: - -```bash -docker-compose up -d -``` - -3. Verify both services are running: - -```bash -docker-compose ps -``` - -## Accessing the Development Environment - -Once your containers are running: - -1. **RushDB Dashboard**: Access at `http://localhost:3000` -2. **Neo4j Browser**: Access at `http://localhost:7474` (if using the local Neo4j setup) - -### Default Credentials - -- **RushDB Dashboard**: - - Username: `admin` - - Password: `password` - -- **Neo4j Browser** (if using local Neo4j): - - Username: `neo4j` - - Password: `password` - -## Advanced Development Workflow - -### 1. Exposing Additional Ports - -If you need to expose additional ports for development: - -```yaml -services: - rushdb: - # ...existing configuration... - ports: - - "3000:3000" - - "9229:9229" # For Node.js debugging -``` - -### 2. Using Local Volume Mounts - -For more extensive development, you might want to mount local files into the container: - -```yaml -services: - rushdb: - # ...existing configuration... - volumes: - - ./your-local-code:/app/platform/core/src -``` - -### 3. Persistent Data Storage - -The default configuration includes volume mounts for Neo4j data persistence. Your data will survive container restarts. - -### Environment Variables - -Before running the container, ensure you provide the following required environment variables: - -- **`NEO4J_URL`**: The connection string for your Neo4j database (e.g., `neo4j+s://.databases.neo4j.io`). -- **`NEO4J_USERNAME`**: The username for accessing the Neo4j database (default is `neo4j`). -- **`NEO4J_PASSWORD`**: The password for your Neo4j database instance. - -### Additional Environment Variables - -#### 1. `RUSHDB_PORT` -- **Description**: The port on which the application server will listen for incoming requests. -- **Default**: `3000` - -#### 2. `RUSHDB_AES_256_ENCRYPTION_KEY` -- **Description**: The encryption key for securing API tokens using AES-256 encryption. -- **Requirement**: Must be exactly 32 characters long to meet the 256-bit key length requirement. -- **Important**: Change this to a secure value in production. -- **Default**: `32SymbolStringForTokenEncryption` - -#### 3. `RUSHDB_LOGIN` -- **Description**: The login username for the RushDB admin account. -- **Important**: Change this to a secure value in production. -- **Default**: `admin` - -#### 4. `RUSHDB_PASSWORD` -- **Description**: The password for the RushDB admin account. -- **Important**: Change this to a secure value in production. -- **Default**: `password` - -#### 5. `SQL_DB_TYPE` -- **Description**: The SQL database engine used for storing dashboard entities (users, workspaces, projects, tokens). -- **Values**: `sqlite` (default, zero-config) or `postgres` (external PostgreSQL) -- **Default**: `sqlite` - -#### 6. `SQL_DB_PATH` -- **Description**: Path to the SQLite database file. Only used when `SQL_DB_TYPE=sqlite`. -- **Default**: `./rushdb.db` - -#### 7. `SQL_DB_URL` -- **Description**: PostgreSQL connection string. Required when `SQL_DB_TYPE=postgres`. -- **Example**: `postgresql://user:password@localhost:5432/rushdb` - -## Working with the RushDB CLI - -The RushDB Docker image includes a command-line interface (CLI) that you can access from the running container. - -### **CLI Commands** - -The RushDB CLI allows you to manage users in self-hosted installations. Below are the available commands: - -#### **Create a New User** - -Command: -```bash -docker exec rushdb rushdb create-user -``` - -Example: -```bash -docker exec rushdb rushdb create-user admin@example.com securepassword123 -``` - -This command creates a new user with the specified login and password. It is only allowed in self-hosted setups. - -#### **Update User Password** - -Command: -```bash -docker exec rushdb rushdb update-password -``` - -Example: -```bash -docker exec rushdb rushdb update-password admin@example.com newsecurepassword456 -``` - -This command updates the password for an existing user identified by the provided login. Like `create-user`, this command is restricted to self-hosted environments. - -## Troubleshooting Common Issues - -### 1. Connection Issues to Neo4j - -If RushDB cannot connect to Neo4j: - -- Verify Neo4j is running: `docker ps | grep neo4j` -- Check Neo4j logs: `docker logs neo4j` -- Ensure credentials match in your environment variables -- If using the local Neo4j setup, ensure the hostname `neo4j` resolves to the Neo4j container - -### 2. RushDB Container Fails to Start - -If the RushDB container exits unexpectedly: - -- Check logs with: `docker logs rushdb` -- Verify all required environment variables are set correctly -- Ensure Neo4j is fully initialized before RushDB attempts to connect - -### 3. Memory Issues - -If containers are being killed due to memory constraints: - -- Increase Docker's memory allocation in Docker Desktop settings -- Consider reducing memory usage in Neo4j configuration -- Use the `--memory` flag with `docker run` or set memory limits in `docker-compose.yml` - -## Next Steps - -After successfully setting up your local development environment: - -1. Explore the RushDB Dashboard at `http://localhost:3000` -2. Create your first project and database -3. Generate API tokens for your applications -4. Explore the API documentation available in the dashboard -5. Connect your applications to RushDB using the available SDKs or REST API - diff --git a/docs/docs/typescript-sdk/_category_.json b/docs/docs/typescript-sdk/_category_.json deleted file mode 100644 index 4791a9ba..00000000 --- a/docs/docs/typescript-sdk/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "TypeScript SDK", - "position": 10, - "collapsed": true, - "collapsible": true -} diff --git a/docs/docs/typescript-sdk/ai/_category_.json b/docs/docs/typescript-sdk/ai/_category_.json deleted file mode 100644 index 0fbfdffd..00000000 --- a/docs/docs/typescript-sdk/ai/_category_.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "label": "AI & Vectors", - "position": 1, - "collapsed": false, - "collapsible": true, - "link": { - "type": "doc", - "id": "typescript-sdk/ai/overview" - } -} diff --git a/docs/docs/typescript-sdk/ai/advanced-indexing.md b/docs/docs/typescript-sdk/ai/advanced-indexing.md deleted file mode 100644 index 6969afb0..00000000 --- a/docs/docs/typescript-sdk/ai/advanced-indexing.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -sidebar_position: 2 -title: Advanced Indexing — BYOV ---- - -# Advanced Indexing — Bring Your Own Vectors - -**External indexes** (BYOV — Bring Your Own Vectors) let you supply pre-computed embedding vectors instead of having the server compute them. Use them when you need: - -- A custom or private model the server cannot access -- Multimodal embeddings (image, audio, document structure) -- Vectors already produced by your ML pipeline -- Reproducible embeddings not tied to the server's active model - ---- - -## Creating an external index - -Pass `external: true` (shorthand) **or** `sourceType: 'external'` (explicit). Both are equivalent: - -```typescript -// ── shorthand ──────────────────────────────────────────────── -const { data: extIndex } = await db.ai.indexes.create({ - label: 'Article', - propertyName: 'body', - external: true, - dimensions: 768, - similarityFunction: 'cosine', -}) -// extIndex.sourceType === 'external' -// extIndex.status === 'awaiting_vectors' - -// ── explicit ───────────────────────────────────────────────── -const { data: extIndex } = await db.ai.indexes.create({ - label: 'Article', - propertyName: 'body', - sourceType: 'external', - dimensions: 768, - similarityFunction: 'cosine', -}) -``` - -An external index starts with status `awaiting_vectors` and transitions to `ready` once at least one vector has been written. - -> Because the server never calls an embedding model , `dimensions` is **required** for external indexes. - -### External vs managed comparison - -| | Managed | External | -|---|---|---| -| `sourceType` | `'managed'` | `'external'` | -| Initial status | `'pending'` | `'awaiting_vectors'` | -| Who computes embeddings | RushDB server (via configured model) | Your application | -| `dimensions` | Optional (uses server default) | **Required** | -| Backfill for existing records | Automatic | Manual via `upsertVectors` or inline writes | - ---- - -## Upsert Vectors - -`db.ai.indexes.upsertVectors()` - -The bulk upload API — ideal for seeding an index from a dataset or syncing after a batch pipeline. - -```typescript -db.ai.indexes.upsertVectors( - indexId: string, - payload: { items: Array<{ recordId: string; vector: number[] }> } -): Promise> -``` - -```typescript -const { data: records } = await db.records.find( - { where: { __label: 'Article' } } -) - -const myEmbedder = new MyEmbeddingModel() -const items = await Promise.all( - records.map(async record => ({ - recordId: record.__id, - vector: await myEmbedder.embed(record.body) - })) -) - -await db.ai.indexes.upsertVectors(extIndex.id, { items }) -``` - -The request is idempotent — calling it again with the same `recordId` **replaces** the stored vector. - ---- - -## Writing vectors at record creation time - -Instead of a two-step create → upsertVectors flow, you can write vectors inline using the `vectors` parameter on any write operation. The server resolves the correct external index automatically. - -See [Write Operations with Vectors](./write-with-vectors.md) for the full reference. - -```typescript -// One-step: create record AND write its vector -const { data: record } = await db.records.create({ - label: 'Article', - data: { title: 'Warp drives', body: 'Alcubierre metric...' }, - vectors: [{ propertyName: 'body', vector: myVec }] -}) -``` - ---- - -## Disambiguation {#disambiguation} - -When the same `(label, propertyName)` pair is covered by more than one external index (different `similarityFunction` or `dimensions`), RushDB cannot determine which index to use without extra information. - -Specify `similarityFunction` to resolve the ambiguity: - -```typescript -// Two indexes on Product:embedding — cosine and euclidean -await db.ai.indexes.create({ - label: 'Product', propertyName: 'embedding', external: true, - similarityFunction: 'cosine', dimensions: 768, -}) -await db.ai.indexes.create({ - label: 'Product', propertyName: 'embedding', external: true, - similarityFunction: 'euclidean', dimensions: 768, -}) - -// ✅ explicit — writes to the cosine index only -await db.records.create({ - label: 'Product', - data: { name: 'Widget' }, - vectors: [{ - propertyName: 'embedding', - vector: vec, - similarityFunction: 'cosine', // <-- required when ambiguous - }] -}) - -// ✅ explicit — searches the euclidean index only -await db.ai.search({ - label: 'Product', - propertyName: 'embedding', - queryVector: vec, - similarityFunction: 'euclidean', // <-- required when ambiguous -}) - -// ❌ omitting similarityFunction when two indexes exist → 422 Unprocessable Entity -await db.records.create({ - label: 'Product', - data: { name: 'Gadget' }, - vectors: [{ propertyName: 'embedding', vector: vec }], -}) -``` - -### Index signature uniqueness - -Two index policies are considered **identical** (and a second `create` returns `409 Conflict`) when all five fields match: - -| Field | Effect on uniqueness | -|---|---| -| `label` | ✅ | -| `propertyName` | ✅ | -| `sourceType` | ✅ | -| `similarityFunction` | ✅ | -| `dimensions` | ✅ | - -Changing any one field produces a distinct index and both are allowed to coexist. - ---- - -## Complete BYOV worked example - -```typescript -import RushDB from '@rushdb/javascript-sdk' - -const db = new RushDB('your-api-key') - -// 1. Create the external index -const { data: idx } = await db.ai.indexes.create({ - label: 'Doc', - propertyName: 'content', - external: true, - dimensions: 3, - similarityFunction: 'cosine', -}) - -// 2. Create records + write inline vectors (one round trip per record) -const articles = [ - { title: 'Alpha', content: 'First article', vector: [1, 0, 0] }, - { title: 'Beta', content: 'Second article', vector: [0, 1, 0] }, - { title: 'Gamma', content: 'Third article', vector: [0, 0, 1] }, -] - -for (const { title, content, vector } of articles) { - await db.records.create({ - label: 'Doc', - data: { title, content }, - vectors: [{ propertyName: 'content', vector }], - }) -} - -// 3. Search using a pre-computed query vector -const { data: results } = await db.ai.search({ - label: 'Doc', - propertyName: 'content', - queryVector: [1, 0, 0], // closest to Alpha - limit: 3, -}) - -console.log(results[0].title) // 'Alpha' -console.log(results[0].__score) // ~1.0 -``` - ---- - -## Batch import with `createMany` - -For bulk seeding with flat rows, use `records.createMany()` with the top-level `vectors` parameter: - -```typescript -await db.records.createMany({ - label: "Doc", - data: [ - { title: "Alpha", content: "First article" }, - { title: "Beta", content: "Second article" }, - { title: "Gamma", content: "Third article" }, - ], - vectors: [ - [{ propertyName: "content", vector: [1, 0, 0] }], - [{ propertyName: "content", vector: [0, 1, 0] }], - [{ propertyName: "content", vector: [0, 0, 1] }], - ], -}) -``` - -For nested JSON payloads, use `importJson` to create records and then call `db.ai.indexes.upsertVectors()` to seed the vectors separately. - ---- - -## Mixing managed and external indexes - -You can have both a managed index and an external index on the same property simultaneously: - -```typescript -// Managed — server embeds for full-text search -await db.ai.indexes.create({ label: 'Product', propertyName: 'description' }) - -// External — your custom multimodal model -await db.ai.indexes.create({ - label: 'Product', propertyName: 'description', - external: true, dimensions: 512, similarityFunction: 'cosine', -}) -``` - -Specifying `similarityFunction` in `db.ai.search()` routes the query to the intended index. diff --git a/docs/docs/typescript-sdk/ai/indexing.md b/docs/docs/typescript-sdk/ai/indexing.md deleted file mode 100644 index 4e34585f..00000000 --- a/docs/docs/typescript-sdk/ai/indexing.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -sidebar_position: 1 -title: Embedding Indexes ---- - -# Embedding Indexes - -An **embedding index** is a policy that tells RushDB to vectorize a specific string property for a label. Once `status` is `ready`, every record matching that label+property pair is searchable via `db.ai.search()`. - ---- - -## How indexes work - -Indexes are scoped to `(label, propertyName)`. "Book:description" and "Article:description" are completely independent — they maintain separate vector stores and never interfere. - -``` -Index policy - label: "Book" - propertyName: "description" - sourceType: "managed" - dimensions: 1536 - status: "ready" - -↓ backfill runs automatically - -Book records get vectors stored on their VALUE relationships: - rel._emb_managed_cosine_1536 = [0.1, 0.2, ...] -``` - -When new records are created or existing records are updated, the index transitions back to `pending` and vectors are recomputed on the next backfill cycle. - ---- - -## List Indexes - -`db.ai.indexes.find()` - -List all embedding index policies for the current project. - -```typescript -const { data: indexes } = await db.ai.indexes.find(); -/* -[ - { - id: "01jb...", - label: "Book", - propertyName: "description", - sourceType: "managed", - similarityFunction: "cosine", - dimensions: 1536, - status: "ready", - modelKey: "text-embedding-3-small", - ... - } -] -*/ -``` - ---- - -## Create Index - -`db.ai.indexes.create()` - -Create a new managed embedding index for a string property. - -```typescript -db.ai.indexes.create(params: { - label: string - propertyName: string - sourceType?: 'managed' | 'external' - similarityFunction?: 'cosine' | 'euclidean' // default: 'cosine' - dimensions?: number // default: server RUSHDB_EMBEDDING_DIMENSIONS -}): Promise> -``` - -```typescript -// Simplest form — uses server-configured model and dimensions -const { data: index } = await db.ai.indexes.create({ - label: "Book", - propertyName: "description", -}); - -console.log(index.status); // 'pending' → backfill starts immediately -``` - -```typescript -// With explicit parameters -const { data: index } = await db.ai.indexes.create({ - label: "Article", - propertyName: "body", - similarityFunction: "cosine", - dimensions: 1536, -}); -``` - -> Attempting to create a duplicate `(label, propertyName, sourceType, similarityFunction, dimensions)` tuple returns `409 Conflict`. - -### Index lifecycle - -| Status | Description | -| ------------------ | ------------------------------------------------------ | -| `pending` | Policy created, waiting for backfill scheduler | -| `indexing` | Backfill in progress | -| `awaiting_vectors` | External index — waiting for client to push vectors | -| `ready` | All existing records have vectors, search is available | -| `error` | Backfill failed; check server logs for the cause | - ---- - -## Get Index Stats - -`db.ai.indexes.stats(id)` - -Returns the fill rate for an index — useful for progress monitoring or health checks. - -```typescript -db.ai.indexes.stats(id: string): Promise> -``` - -```typescript -const { data: stats } = await db.ai.indexes.stats(index.id); -console.log(`${stats.indexedRecords} / ${stats.totalRecords} records indexed`); -``` - -```typescript -type EmbeddingIndexStats = { - totalRecords: number; - indexedRecords: number; -}; -``` - ---- - -## Delete Index - -`db.ai.indexes.delete(id)` - -Remove an embedding index policy and its scoped vector data. - -```typescript -await db.ai.indexes.delete(index.id); -``` - -The underlying Neo4j DDL vector index is only dropped when **zero embeddings remain** across the entire project. This avoids unnecessary index rebuilds when multiple policies share the same `(dimensions, similarityFunction)` combination. - ---- - -## Response type - -```typescript -type EmbeddingIndex = { - id: string; - projectId: string; - /** Neo4j label this index is scoped to (e.g. "Book"). */ - label: string; - propertyName: string; - modelKey: string; - sourceType: "managed" | "external"; - similarityFunction: "cosine" | "euclidean"; - dimensions: number; - vectorPropertyName: string; // internal Neo4j property name for the vector - enabled: boolean; - status: string; - createdAt: string; - updatedAt: string; -}; -``` - ---- - -## Waiting for an index to become ready - -For managed indexes, backfill runs asynchronously. Poll `db.ai.indexes.find()` until status is `ready`: - -```typescript -async function waitForIndexReady( - db: RushDB, - indexId: string, - timeoutMs = 90_000, -): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - const { data: indexes } = await db.ai.indexes.find(); - const idx = indexes.find((i) => i.id === indexId); - if (idx?.status === "ready") return; - if (idx?.status === "error") throw new Error("Index entered error state"); - await new Promise((r) => setTimeout(r, 3_000)); - } - throw new Error("Index did not become ready in time"); -} - -const { data: index } = await db.ai.indexes.create({ - label: "Book", - propertyName: "description", -}); -await waitForIndexReady(db, index.id); -// now safe to call db.ai.search(...) -``` - ---- - -## Multiple indexes on the same property - -You can have more than one index per `(label, propertyName)` pair, provided the signature differs: - -```typescript -// Same label + property, different similarity function -await db.ai.indexes.create({ - label: "Product", - propertyName: "description", - similarityFunction: "cosine", - dimensions: 768, -}); - -await db.ai.indexes.create({ - label: "Product", - propertyName: "description", - similarityFunction: "euclidean", - dimensions: 768, -}); -``` - -When performing a search or writing inline vectors against a property with multiple indexes, specify `similarityFunction` to disambiguate. See [Advanced Indexing — BYOV](./advanced-indexing.md#disambiguation) for details. - ---- - -## String Array Properties - -`List` - -String array properties are supported. Each item in the array is embedded individually, then mean-pooled into a single vector stored on the relationship. diff --git a/docs/docs/typescript-sdk/ai/overview.md b/docs/docs/typescript-sdk/ai/overview.md deleted file mode 100644 index 09702202..00000000 --- a/docs/docs/typescript-sdk/ai/overview.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -sidebar_position: 0 -title: Overview ---- - -# AI & Semantic Search - -RushDB is a **self-aware memory layer for agents, humans, and apps**. It continuously understands its own structure — labels, fields, value distributions, relationships — and exposes that knowledge so that agents can reason over real data without hallucinating schema details, and apps can retrieve semantically relevant context on demand. - -The `db.ai` namespace covers three capabilities: - -| Capability | Description | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| **Graph Ontology** | Self-describing schema discovery: label names, field types, value ranges, and the relationship map — always up to date | -| **Embedding Indexes** | Per-label vector policies that turn string properties into long-term semantic memory | -| **Semantic Search** | Cosine/euclidean similarity retrieval over indexed properties, for agents and apps alike | - ---- - -## How it fits together - -``` -┌─────────────────────────────────────────────────────┐ -│ Your data (records + relationships) │ -│ │ -│ BOOK { title: "...", description: "..." } │ -└────────────────────┬────────────────────────────────┘ - │ - db.ai.indexes.create() - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Embedding index policy │ -│ label: BOOK property: description dims: 1536 │ -│ sourceType: managed | external │ -└────────────────────┬────────────────────────────────┘ - │ - Backfill (managed) / inline vectors (external) - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Vector stored on VALUE relationship │ -│ rel._emb_managed_cosine_1536 = [0.1, 0.2, ...] │ -└────────────────────┬────────────────────────────────┘ - │ - db.ai.search({ query / queryVector }) - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ DBRecordsArrayInstance — records ranked by score │ -│ result.data.__score = 0.94 (cosine similarity) │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## Quick links - -| Topic | Description | -| -------------------------------------------------- | ----------------------------------------------------------- | -| [Ontology](./overview.md#graph-ontology) | Schema discovery with `getOntology` / `getOntologyMarkdown` | -| [Indexing](./indexing.md) | Create and manage managed embedding indexes | -| [Advanced indexing — BYOV](./advanced-indexing.md) | Bring Your Own Vectors: external indexes, inline writes | -| [Semantic search](./search.md) | Query by meaning with `db.ai.search()` | -| [Writing with vectors](./write-with-vectors.md) | Attach vectors at create / upsert / importJson time | -| [Agent Skills](#agent-skills) | Installable skills that teach any compatible agent to use RushDB | - ---- - -## Graph Ontology - -The ontology endpoints expose a live snapshot of your database structure — without any manual schema definitions. - -### Get Ontology as Markdown - -`db.ai.getOntologyMarkdown()` - -Returns the full schema as compact Markdown — the **recommended format for LLM context injection**. - -```typescript -db.ai.getOntologyMarkdown( - params?: { labels?: string[]; force?: boolean }, - transaction?: Transaction | string -): Promise> -``` - -```typescript -// Inject into LLM at session start -const { data: schema } = await db.ai.getOntologyMarkdown(); -const messages = [ - { role: "system", content: `You are a data assistant.\n\n${schema}` }, - { role: "user", content: "How many paid orders are there?" }, -]; - -// Scope to specific labels -const { data: orderSchema } = await db.ai.getOntologyMarkdown({ - labels: ["Order"], -}); - -// Bypass the 1-hour cache and force a fresh recalculation -const { data: freshSchema } = await db.ai.getOntologyMarkdown({ force: true }); -``` - -
-Example output - -```text -# Graph Ontology - -## Labels - -| Label | Count | -|-----------|------:| -| `Order` | 1840 | -| `User` | 312 | -| `Product` | 95 | - ---- - -## `Order` (1840 records) - -### Properties - -| Property | Type | Values / Range | Semantic Search | -|-------------|----------|----------------------------------------|--------------------------------| -| `status` | string | `pending`, `paid`, `shipped` (+2 more) | — | -| `total` | number | `4.99`..`2499.00` | — | -| `name` | string | `Widget A`, `Widget B` (+8 more) | `managed` cosine 1536d [ready] | -| `createdAt` | datetime | `2024-01-03`..`2026-02-27` | — | - -### Relationships - -| Type | Direction | Other Label | -|-------------|-----------|-------------| -| `PLACED_BY` | out | `User` | -| `CONTAINS` | out | `Product` | -``` - -
- ---- - -### Get Ontology (raw) - -`db.ai.getOntology()` - -Returns the same ontology as a structured JSON array — useful for schema UIs, auto-complete, or looking up property IDs for `db.properties.values()`. - -```typescript -db.ai.getOntology( - params?: { labels?: string[]; force?: boolean }, - transaction?: Transaction | string -): Promise> -``` - -```typescript -// List all labels with counts -const { data: ontology } = await db.ai.getOntology(); -for (const item of ontology) { - console.log(`${item.label}: ${item.count} records`); -} - -// Get property ID for value enumeration -const { - data: [bookSchema], -} = await db.ai.getOntology({ labels: ["Book"] }); -const genreProp = bookSchema.properties.find((p) => p.name === "genre"); -const { data: genres } = await db.properties.values({ id: genreProp.id }); - -// Identify semantically-searchable properties -const indexed = bookSchema.properties.filter((p) => p.vectorIndexes?.length); -// indexed[0].vectorIndexes[0].status === 'ready' → queryable with db.ai.search() - -// Bypass the 1-hour cache -const { data: fresh } = await db.ai.getOntology({ force: true }); -``` - -```typescript -type OntologyItem = { - label: string; - count: number; - properties: OntologyProperty[]; - relationships: OntologyRelationship[]; -}; - -type OntologyVectorIndex = { - id: string; - sourceType: string; // 'managed' | 'external' - similarityFunction: string; // 'cosine' | 'euclidean' - dimensions: number; - status: string; // 'pending' | 'indexing' | 'ready' | 'error' - modelKey: string; -}; - -type OntologyProperty = { - id: string; // use with db.properties.values() - name: string; - type: string; // 'string' | 'number' | 'boolean' | 'datetime' - values?: Array; // up to 10 samples (string/boolean only) - min?: number | string; // number/datetime only - max?: number | string; - /** Non-empty when embedding indexes exist — property is queryable with db.ai.search() */ - vectorIndexes?: OntologyVectorIndex[]; -}; - -type OntologyRelationship = { - label: string; - type: string; - direction: "in" | "out"; -}; -``` - -:::note Caching -Both methods share a **1-hour cache** per project. The first call after TTL expiry triggers a full graph scan; all subsequent calls within the hour are instant. Pass `{ force: true }` to bypass the cache and trigger an immediate recalculation. -::: - -:::tip Agent quickstart -Call `db.ai.getOntologyMarkdown()` first in every AI session. Without it, models will hallucinate field and label names. -::: - ---- - -## Agent Skills - -`@rushdb/skills` is a collection of [Agent Skills](https://agentskills.io) — installable instructions that teach any skills-compatible AI agent (Claude, GitHub Copilot, Cursor, Windsurf, and others) to use RushDB efficiently, without manual system prompt engineering. - -```bash -npx skills add rush-db/rushdb --path packages/skills -``` - -| Skill | What it teaches | -|---|---| -| `rushdb-query-builder` | Discovery-first workflow, SearchQuery syntax, aggregation, relationship traversal, and semantic search | -| `rushdb-agent-memory` | Using RushDB as persistent structured memory — store, link, and semantically recall sessions, decisions, and entities | -| `rushdb-data-modeling` | LMPG model design, label/property naming conventions, nested JSON import, and schema evolution | -| `rushdb-faceted-search` | Build faceted filter UIs — discover properties and types, enumerate distinct values, map to widgets, assemble a live `where` clause | - -Each skill bundles a `SKILL.md` with concise instructions and optional reference files (like the full SearchQuery spec) that the agent loads on demand. - -:::note MCP server vs. Agent Skills -The [MCP server](/mcp-server/introduction) gives agents direct tool access to RushDB at runtime. Agent Skills teach agents *how* to use those tools correctly — they complement each other. -::: diff --git a/docs/docs/typescript-sdk/ai/search.md b/docs/docs/typescript-sdk/ai/search.md deleted file mode 100644 index 7f36527d..00000000 --- a/docs/docs/typescript-sdk/ai/search.md +++ /dev/null @@ -1,203 +0,0 @@ ---- -sidebar_position: 3 -title: Semantic Search ---- - -# Semantic Search - -`db.ai.search()` performs semantic vector search across records that have an associated embedding index. - ---- - -## Signature - -```typescript -db.ai.search(params: { - /** One or more Neo4j labels to search within (e.g. "Book"). */ - labels: string[] - - /** Property the target embedding index is scoped to. */ - propertyName: string - - /** Natural-language query — used by managed indexes. */ - query?: string - - /** Pre-computed query vector — used by external indexes. */ - queryVector?: number[] - - /** Override the similarity function when multiple indexes match. */ - similarityFunction?: 'cosine' | 'euclidean' - - /** Override dimensions when multiple indexes match. */ - dimensions?: number - - /** Prefilter: only return records that also satisfy this where clause. */ - where?: WhereClause - - /** Maximum results to return. */ - limit?: number - - /** Results to skip (for pagination). */ - skip?: number -}): Promise> -``` - ---- - -## Result type - -```typescript -// Each result is a DBRecordInstance — data lives on .data -type DBRecordInstance = { - /** The unique record ID. */ - id: string - /** The record label. */ - label: string - /** Raw record data including score. */ - data: { - __id: string - __label: string - /** Similarity score (0–1, higher = more similar). Only present on ai.search results. */ - __score?: number - [key: string]: unknown - } -} -``` - -Results are always ordered by `__score` descending — closest match first. - ---- - -## Managed search (query text) - -For a **managed** index, pass `query` (a natural-language string). The server embeds it using the same model that was used when building the index, then ranks the prefiltered candidates by similarity. - -```typescript -const { data: results } = await db.ai.search({ - labels: ['Book'], - propertyName: 'description', - query: 'space exploration and interstellar travel', - limit: 5, -}) - -results.forEach(r => { - console.log(`[${r.data.__score!.toFixed(4)}] ${r.data.title}`) -}) -``` - ---- - -## External search (query vector) - -For an **external** index, pass `queryVector` — a pre-computed embedding produced by your own model. No text is sent to the server. - -```typescript -const myEmbedder = new MyEmbeddingModel() -const vec = await myEmbedder.embed('space exploration') - -const { data: results } = await db.ai.search({ - labels: ['Article'], - propertyName: 'body', - queryVector: vec, - limit: 10, -}) -``` - -- `query` is **not allowed** with external indexes — the server has no model to embed it. -- `queryVector` is **not required** for managed indexes but is accepted (bypasses server embedding). - -### Dimension inference - -When `queryVector` is supplied you can omit `dimensions` — the server infers it from `queryVector.length`: - -```typescript -// dimensions is optional when queryVector is given -await db.ai.search({ - labels: ['Product'], - propertyName: 'embedding', - queryVector: [0.1, 0.9, 0.4], // length 3 → dimensions inferred as 3 -}) -``` - ---- - -## Filtering with `where` - -The `where` clause acts as a **prefilter** — only records that satisfy the filter are candidates for similarity ranking. RushDB already scopes search to the current project, and `where` adds your application-level constraints before scoring. - -```typescript -const { data: results } = await db.ai.search({ - labels: ['Product'], - propertyName: 'description', - query: 'wireless headphones', - where: { - category: { $eq: 'electronics' }, - inStock: { $eq: true }, - price: { $lt: 100 }, - }, - limit: 20, -}) -``` - -All `WhereClause` operators supported by `db.records.find()` are available here. - ---- - -## Multi-label search - -Pass an array of labels to search across multiple entity types simultaneously: - -```typescript -const { data: results } = await db.ai.search({ - labels: ['Article', 'Post', 'Comment'], - propertyName: 'body', - query: 'machine learning trends', - limit: 10, -}) - -// Each result carries .data.__label so you can tell them apart -results.forEach(r => console.log(r.data.__label, r.data.__score, r.data.title ?? r.data.text)) -``` - -All listed labels must have an embedding index on the same `propertyName`, or the request will return `404` for the missing labels. - ---- - -## Disambiguation - -When two indexes exist for the same `(label, propertyName)`, you must specify `similarityFunction` (and optionally `dimensions`) to select the target index: - -```typescript -// Two indexes: Product:embedding/cosine and Product:embedding/euclidean -await db.ai.search({ - labels: ['Product'], - propertyName: 'embedding', - queryVector: vec, - similarityFunction: 'cosine', // required — otherwise 422 Unprocessable Entity -}) -``` - ---- - -## Pagination - -```typescript -const PAGE = 20 - -// Page 1 -const { data: page1 } = await db.ai.search({ ..., limit: PAGE, skip: 0 }) -// Page 2 -const { data: page2 } = await db.ai.search({ ..., limit: PAGE, skip: PAGE }) -``` - ---- - -## Error reference - -| HTTP | Cause | -|---|---| -| `404 Not Found` | No enabled embedding index found for `(label, propertyName)` | -| `422 Unprocessable Entity` | Multiple indexes match and `similarityFunction` was not specified | -| `422 Unprocessable Entity` | `query` text supplied for an external index (server cannot embed it) | -| `422 Unprocessable Entity` | Vector length does not match index `dimensions` | -| `503 Service Unavailable` | Embedding model unavailable (managed indexes only) | diff --git a/docs/docs/typescript-sdk/ai/write-with-vectors.md b/docs/docs/typescript-sdk/ai/write-with-vectors.md deleted file mode 100644 index 95512df3..00000000 --- a/docs/docs/typescript-sdk/ai/write-with-vectors.md +++ /dev/null @@ -1,322 +0,0 @@ ---- -sidebar_position: 4 -title: Writing Records with Vectors ---- - -# Writing Records with Vectors - -RushDB lets you attach pre-computed embedding vectors to records **at write time**, eliminating the need for a separate `upsertVectors` call. Any operation that creates or modifies records supports this through the `vectors` parameter. - -This feature requires at least one [external index](./advanced-indexing.md) to exist for the target `(label, propertyName)`. - ---- - -## `vectors` parameter - -All write operations accept a `vectors` array: - -```typescript -type VectorEntry = { - /** Property name this vector is associated with. */ - propertyName: string - /** Pre-computed embedding vector. */ - vector: number[] - /** Required when multiple indexes exist on the same property. */ - similarityFunction?: 'cosine' | 'euclidean' -} -``` - ---- - -## Create a Record with Vectors - -`records.create()` - -```typescript -const { data: record } = await db.records.create({ - label: 'Article', - data: { - title: 'How transformers work', - body: 'Attention is all you need ...', - }, - vectors: [ - { propertyName: 'body', vector: myEmbed('Attention is all you need ...') } - ], -}) - -console.log(record.__id) // record is created AND vector is written atomically -``` - ---- - -## Upsert with Vectors - -`records.upsert()` - -`upsert` is idempotent on the record's slug (natural key). Passing `vectors` writes (or replaces) the stored vector for each `propertyName` in the same call: - -```typescript -// First call — creates the record + writes vector -const { data: r1 } = await db.records.upsert({ - label: 'Article', - data: { slug: 'transformers-101', title: 'Transformers 101', body: '...' }, - vectors: [{ propertyName: 'body', vector: v1 }], -}) - -// Second call — same slug → updates the title/body + replaces the vector -const { data: r2 } = await db.records.upsert({ - label: 'Article', - data: { slug: 'transformers-101', title: 'Transformers 101 (revised)', body: 'Updated ...' }, - vectors: [{ propertyName: 'body', vector: v2 }], -}) - -console.log(r1.__id === r2.__id) // true — same record -``` - ---- - -## Set with Vectors - -`records.set()` - -`set` replaces all properties of a record with new values. Including `vectors` writes those vectors at the same time: - -```typescript -// Find or create the record first -const { data: rec } = await db.records.create({ - label: 'Product', - data: { name: 'Widget', price: 9.99 }, -}) - -// Full replace — data AND vector updated together -await db.records.set(rec.__id, { - data: { name: 'Widget Pro', price: 19.99 }, - vectors: [{ propertyName: 'description', vector: newVec }], -}) -``` - ---- - -## Create Multiple Records with Vectors - -`records.createMany()` - -`createMany` is optimised for flat (CSV-like) rows. Use the top-level `vectors` parameter — an array indexed by row position — to attach a vector to each record without nesting arrays inside your flat data: - -```typescript -await db.records.createMany({ - label: 'Product', - data: [ - { name: 'Alpha', description: 'First product' }, - { name: 'Beta', description: 'Second product' }, - { name: 'Gamma', description: 'Third product' }, - ], - vectors: [ - [{ propertyName: 'description', vector: [1, 0, 0] }], // row 0 - [{ propertyName: 'description', vector: [0, 1, 0] }], // row 1 - [{ propertyName: 'description', vector: [0, 0, 1] }], // row 2 - ], - options: { returnResult: true }, -}) -``` - -### Sparse vectors - -Leave rows without vectors by providing a shorter `vectors` array (any unspecified trailing rows are skipped): - -```typescript -await db.records.createMany({ - label: 'Product', - data: [{ name: 'Alpha' }, { name: 'Beta' }, { name: 'Gamma' }], - // only row 0 gets a vector; rows 1 and 2 are skipped - vectors: [[{ propertyName: 'description', vector: myVec }]], -}) -``` - -### Validation - -The SDK throws synchronously if `vectors.length > data.length`: - -```typescript -// ❌ Throws: "vectors length (3) exceeds the number of data rows (2)" -db.records.createMany({ - label: 'Product', - data: [{ name: 'A' }, { name: 'B' }], - vectors: [ - [{ propertyName: 'description', vector: [1, 0, 0] }], - [{ propertyName: 'description', vector: [0, 1, 0] }], - [{ propertyName: 'description', vector: [0, 0, 1] }], // no row 2 - ], -}) -``` - ---- - -## Import CSV with Vectors - -`records.importCsv()` - -CSV data is a raw string, so per-row vectors are supplied as a separate `vectors` parameter using the same indexed-array format as `createMany`. Row indices are 0-based and refer to data rows after the header is consumed. - -```typescript -const csv = `name,description -Alpha,First product -Beta,Second product -Gamma,Third product` - -await db.records.importCsv({ - label: 'Product', - data: csv, - vectors: [ - [{ propertyName: 'description', vector: [1, 0, 0] }], // csv row 0 - [{ propertyName: 'description', vector: [0, 1, 0] }], // csv row 1 - [{ propertyName: 'description', vector: [0, 0, 1] }], // csv row 2 - ], - options: { returnResult: true }, -}) -``` - -### Sparse vectors - -Same sparse pattern as `createMany` — any rows beyond `vectors.length` get no vector: - -```typescript -await db.records.importCsv({ - label: 'Product', - data: csv, - // only the first row gets a vector - vectors: [[{ propertyName: 'description', vector: myVec }]], -}) -``` - -### Validation - -The server returns `400 Bad Request` if `vectors.length` exceeds the number of data rows (validated after CSV parsing). The client does not know the row count before sending since CSV is a raw string. - -``` -400 Bad Request: vectors length (5) exceeds the number of CSV data rows (3) -``` - ---- - -## Specifying `similarityFunction` for disambiguation - -When a single `(label, propertyName)` has multiple external indexes registered (e.g. one cosine and one euclidean), you must include `similarityFunction` in each `VectorEntry` so the server can route the write to the correct index: - -```typescript -// Write to the cosine index -await db.records.create({ - label: 'Product', - data: { name: 'Widget' }, - vectors: [ - { propertyName: 'embedding', vector: vec, similarityFunction: 'cosine' } - ], -}) -``` - -Omitting `similarityFunction` when multiple indexes match returns `422 Unprocessable Entity`. - ---- - -## Multiple vectors in one call - -You can write vectors for multiple properties or indexes in a single operation: - -```typescript -await db.records.create({ - label: 'Document', - data: { title: 'Multi-modal doc', abstract: '...', fullText: '...' }, - vectors: [ - { propertyName: 'abstract', vector: abstractVec }, - { propertyName: 'fullText', vector: fullTextVec }, - ], -}) -``` - -Each entry is matched independently against the available external indexes. - ---- - -## Complete worked example - -```typescript -import RushDB from '@rushdb/javascript-sdk' - -const db = new RushDB('your-api-key') -const emb = new YourEmbeddingModel() - -// 1. Create an external index once (idempotent via 409 Conflict) -const { data: idx } = await db.ai.indexes.create({ - label: 'Article', - propertyName: 'body', - external: true, - dimensions: 768, - similarityFunction: 'cosine', -}).catch(e => e.status === 409 ? db.ai.indexes.find() : Promise.reject(e)) - -// 2. Create records from your pipeline, embedding as you go -const docs = [ - { title: 'Alpha', body: 'First doc' }, - { title: 'Beta', body: 'Second doc' }, -] - -for (const doc of docs) { - await db.records.create({ - label: 'Article', - data: doc, - vectors: [{ propertyName: 'body', vector: await emb.embed(doc.body) }], - }) -} - -// 3. Search -const queryVec = await emb.embed('first document') -const { data } = await db.ai.search({ - label: 'Article', - propertyName: 'body', - queryVector: queryVec, - limit: 3, -}) -console.log(data[0].title) // 'Alpha' -``` - ---- - -## Inline vectors vs. `upsertVectors` - -| | Inline `vectors` | `db.ai.indexes.upsertVectors()` | -|---|---|---| -| **Round trips** | 1 (write + vector together) | 2+ (write, then upload) | -| **Use case** | Streaming ingestion, real-time pipeline | Batch backfill, dataset migration | -| **Idempotency** | Depends on the write operation used | Always idempotent per `recordId` | -| **Availability** | `create`, `upsert`, `set`, `createMany`, `importCsv` | Standalone call on any existing records | -| **Multi-record** | `createMany` or `importCsv` with indexed `vectors[][]` | Single bulk payload | - -For streaming pipelines that produce records one-by-one or in small batches, inline vectors are simpler and more efficient. For seeding an index from a large existing dataset, `upsertVectors` is the right choice. - ---- - -## Vector format by method - -| Method | Vector syntax | Notes | -|---|---|---| -| `create` | `vectors: VectorEntry[]` | single record | -| `upsert` | `vectors: VectorEntry[]` | single record, idempotent | -| `set` | `vectors: VectorEntry[]` | single record, full replace | -| `createMany` | `vectors: VectorEntry[][]` (indexed) | `vectors[i]` → `data[i]` | -| `importCsv` | `vectors: VectorEntry[][]` (indexed) | `vectors[i]` → CSV row `i` | - -`createMany` and `importCsv` use an external indexed array so that each row's vector is unambiguously matched by position. For nested JSON imports use `importJson` to create the records, then call `db.ai.indexes.upsertVectors()` to seed the vectors separately. - ---- - -## Error conditions - -| Error | Cause | Method | -|---|---|---| -| `404 Not Found` | No external index exists for `(label, propertyName)` | all | -| `422 Unprocessable Entity` | `vector.length` does not match `index.dimensions` | all | -| `422 Unprocessable Entity` | Multiple indexes match and `similarityFunction` was not specified | all | -| `400 Bad Request` | `vectors.length` exceeds number of CSV data rows | `importCsv` | -| Client `Error` | `vectors.length` exceeds `data.length` | `createMany` (thrown synchronously) | - -> `importJson` does not accept a `vectors` parameter. Use `createMany` for flat rows with inline vectors, or use `importJson` followed by `db.ai.indexes.upsertVectors()` for nested JSON payloads. diff --git a/docs/docs/typescript-sdk/introduction.md b/docs/docs/typescript-sdk/introduction.md deleted file mode 100644 index f69cad3a..00000000 --- a/docs/docs/typescript-sdk/introduction.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -sidebar_position: 0 -title: Introduction ---- - -# TypeScript / JavaScript SDK - -Push JSON, query by value or meaning, traverse graphs — from Node.js or the browser. - -## Install - -```bash -npm install @rushdb/javascript-sdk -# or: yarn add @rushdb/javascript-sdk | pnpm add @rushdb/javascript-sdk -``` - -## Connect - -```typescript -import RushDB from '@rushdb/javascript-sdk' - -const db = new RushDB('RUSHDB_API_KEY') -``` - -Get your API token from the [RushDB Dashboard](https://app.rushdb.com/). - -## First write - -```typescript -// Nested objects become linked records automatically -await db.records.importJson({ - label: 'MOVIE', - data: { - title: 'Inception', - rating: 8.8, - genre: 'sci-fi', - ACTOR: [ - { name: 'Leonardo DiCaprio', country: 'USA' }, - { name: 'Ken Watanabe', country: 'Japan' } - ] - } -}) -// Created: MOVIE → ACTOR × 2 (relationships wired automatically) -``` - -## First read - -```typescript -const { data: movies, total } = await db.records.find({ - labels: ['MOVIE'], - where: { rating: { $gte: 8 } }, - orderBy: { rating: 'desc' } -}) -``` - -## Configuration - -```typescript -const db = new RushDB('RUSHDB_API_KEY', { - url: 'http://localhost:3000/api/v1', // or use host/port/protocol - timeout: 5000 // default: 30000ms -}) -``` - -| Option | Default | Description | -|---|---|---| -| `url` | — | Full API URL (alternative to host/port/protocol) | -| `host` | — | Domain or IP | -| `port` | 80 / 443 | Port number | -| `protocol` | `https` | `http` or `https` | -| `timeout` | `30000` | Request timeout in ms | -| `httpClient` | — | Custom HTTP client — required for Edge / Cloudflare Workers | -| `logger` | — | Custom logging function | -| `options.allowForceDelete` | `false` | Must be `true` to delete all records without criteria (safety gate) | - -## Namespaces - -| Namespace | Use | -|---|---| -| `db.records` | Create, find, update, delete records | -| `db.relationships` | Attach and detach edges | -| `db.tx` | Transactions | -| `db.labels` | List labels and counts | -| `db.properties` | Inspect field names, types, value ranges | -| `db.ai` | Schema export + semantic search | diff --git a/docs/docs/typescript-sdk/labels.md b/docs/docs/typescript-sdk/labels.md deleted file mode 100644 index 8fdb0df2..00000000 --- a/docs/docs/typescript-sdk/labels.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Labels - -List which labels exist in your project and how many records each has. - -## Find Labels - -`db.labels.find()` - -Returns `{ [label]: count }` for records matching the optional filter. - -```typescript -// All labels -const { data } = await db.labels.find(); -// { MOVIE: 84, ACTOR: 312, DIRECTOR: 47 } - -// Labels for records where rating > 8 -const { data } = await db.labels.find({ - where: { rating: { $gt: 8 } }, -}); -// { MOVIE: 21 } -``` - -`find()` accepts the same `where`, `skip`, `limit` parameters as `db.records.find()`. diff --git a/docs/docs/typescript-sdk/models.md b/docs/docs/typescript-sdk/models.md deleted file mode 100644 index ee2474b2..00000000 --- a/docs/docs/typescript-sdk/models.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Models - -A `Model` binds a label to a schema, giving you typed access to all record operations without ever repeating the label name. - -## Define a model - -```typescript -import RushDB, { Model } from '@rushdb/javascript-sdk' - -const db = new RushDB('RUSHDB_API_KEY') - -const MovieModel = new Model('MOVIE', { - title: { type: 'string' }, - rating: { type: 'number' }, - genre: { type: 'string' }, - releasedAt:{ type: 'datetime', default: () => new Date().toISOString() } -}) -``` - -### Schema field types - -| Type | Notes | -|---|---| -| `boolean` | | -| `datetime` | ISO string or detailed object | -| `null` | | -| `number` | | -| `string` | | - -### Schema field options - -| Option | Description | -|---|---| -| `type` | **Required.** Field data type | -| `default` | Static value or `() => value` function (sync or async) | -| `multiple` | `true` → field holds an array | -| `required` | `true` → create throws if value missing | -| `unique` | `true` → value must be unique across all records of this label | - -## Types from the model - -```typescript -export const USER = 'USER' as const - -export const UserModel = new Model(USER, { - name: { type: 'string' }, - login: { type: 'string', unique: true }, - password: { type: 'string' }, - active: { type: 'boolean', default: true }, - createdAt: { type: 'datetime', default: () => new Date().toISOString() }, - tags: { type: 'string', multiple: true, required: false }, -}) - -// Export strongly-typed aliases -export type UserRecord = typeof UserModel.record -export type UserRecordDraft = typeof UserModel.draft -export type UserSearchQuery = SearchQuery -``` - -| Helper | What it represents | -|---|---| -| `.record` | Full DB record including system fields (`__id`, `__label`, `__proptypes`) | -| `.draft` | Your schema fields only — no system fields; use when creating records | -| `.recordInstance` | Record + instance methods (`update`, `delete`, `attach`, …) | -| `.recordsArrayInstance` | Array result with `data` + `total` | - -## CRUD operations - -### Create - -```typescript -const movie = await MovieModel.create({ - title: 'Inception', rating: 8.8, genre: 'sci-fi' -}) - -const movies = await MovieModel.createMany([ - { title: 'The Dark Knight', rating: 9.0, genre: 'action' }, - { title: 'Interstellar', rating: 8.6, genre: 'sci-fi' } -]) -``` - -### Read - -```typescript -const all = await MovieModel.find() -const sciFi = await MovieModel.find({ where: { genre: 'sci-fi' } }) -const one = await MovieModel.findOne({ where: { title: 'Inception' } }) -const byId = await MovieModel.findById('movie-id-123') -const unique = await MovieModel.findUniq({ where: { title: 'Inception' } }) -``` - -### Update - -```typescript -// Partial update — only listed fields change -await MovieModel.update('movie-id-123', { rating: 9.1 }) - -// Full replace — all other fields are removed -await MovieModel.set('movie-id-123', { title: 'Inception', rating: 9.1, genre: 'sci-fi' }) -``` - -### Delete - -```typescript -await MovieModel.delete({ where: { genre: 'temp' } }) -await MovieModel.deleteById(['movie-id-123', 'movie-id-456']) -``` - -### Relationships - -```typescript -await MovieModel.attach({ - source: 'movie-id-123', - target: 'actor-id-456', - options: { type: 'STARS', direction: 'out' } -}) - -await MovieModel.detach({ - source: 'movie-id-123', - target: 'actor-id-456', - options: { type: 'STARS' } -}) -``` - -## Initialization order - -Create the `RushDB` instance before importing models — models call `RushDB.getInstance()` on first use. - -```typescript -// db.ts -import RushDB from '@rushdb/javascript-sdk' -export const db = new RushDB('RUSHDB_API_KEY') - -// models.ts — import db.ts first in your app entry -import './db' -import { Model } from '@rushdb/javascript-sdk' -export const MovieModel = new Model('MOVIE', { /* … */ }) -``` - -## Transactions - -```typescript -const tx = await db.tx.begin() -try { - const movie = await MovieModel.create({ title: 'Dune', rating: 8.0, genre: 'sci-fi' }, tx) - const actor = await ActorModel.create({ name: 'Timothée Chalamet' }, tx) - await MovieModel.attach({ source: movie, target: actor, options: { type: 'STARS' } }, tx) - await tx.commit() -} catch (e) { - await tx.rollback() - throw e -} -``` - -## Advanced TypeScript - -For declaration merging, path aliases, and schema-aware intellisense (typed relation queries, aggregate result shapes), see the [Model reference](./typescript-reference/Model#typescript-extend-sdk-types-for-schema-aware-suggestions). diff --git a/docs/docs/typescript-sdk/properties.md b/docs/docs/typescript-sdk/properties.md deleted file mode 100644 index 927f279c..00000000 --- a/docs/docs/typescript-sdk/properties.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Properties - -Inspect and manage field definitions across your project. - -## Find Properties - -`db.properties.find()` - -Returns all property definitions matching the filter. - -```typescript -const { data } = await db.properties.find({ - where: { type: "number" }, -}); -// [{ id, name: 'rating', type: 'number', ... }, ...] -``` - -## Find by ID - -`db.properties.findById()` - -```typescript -const prop = await db.properties.findById("property-id"); -``` - -## Get Property Values - -`db.properties.values()` - -Enumerate distinct values for a property — useful for building filter UIs. - -```typescript -const { data: genres } = await db.properties.values("prop-id-genre"); -// ['sci-fi', 'action', 'drama', ...] - -// With filter -const { data } = await db.properties.values("prop-id", { - query: "sci", // text prefix filter - orderBy: "asc", - limit: 10, -}); -``` - -## Delete Property - -`db.properties.delete()` - -```typescript -await db.properties.delete("property-id"); -``` - -Deletes the property definition and removes it from all records that use it. -type: 'string', diff --git a/docs/docs/typescript-sdk/raw-queries.md b/docs/docs/typescript-sdk/raw-queries.md deleted file mode 100644 index 73f0a1d6..00000000 --- a/docs/docs/typescript-sdk/raw-queries.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -sidebar_position: 8 ---- - -# Raw Queries - -:::warning Requires a connected Neo4j instance -This endpoint is only available when your project is connected to your own Neo4j database. Connecting a custom Neo4j instance is available on the free tier — see the RushDB dashboard to set it up. -::: - -Use this endpoint to run arbitrary Cypher queries against your connected Neo4j database. This is intended for advanced use-cases and requires the managed service or a custom DB connection. - -### TypeScript SDK example - -```ts -const result = await db.query.raw({ - query: 'MATCH (n:Person) RETURN n LIMIT $limit', - params: { limit: 10 } -}) - -// `result` contains the server response with query records as returned by Neo4j driver -console.log(result) -``` - -### Real-world example: employees at a company - -This example shows a parameterized query that finds people employed by a company and returns selected fields. - -```ts -const company = 'Acme Corp' -const result = await db.query.raw({ - query: ` - MATCH (c:Company { name: $company })<-[:EMPLOYS]-(p:Person) - RETURN p { .name, .email, company: c.name } AS employee - ORDER BY p.name - LIMIT $limit - `, - params: { company, limit: 50 } -}) - -console.log(result.data) -``` diff --git a/docs/docs/typescript-sdk/records/_category_.json b/docs/docs/typescript-sdk/records/_category_.json deleted file mode 100644 index ebf0e246..00000000 --- a/docs/docs/typescript-sdk/records/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Records", - "position": 2, - "collapsed": false, - "collapsible": false -} diff --git a/docs/docs/typescript-sdk/records/create-records.md b/docs/docs/typescript-sdk/records/create-records.md deleted file mode 100644 index 59d7a659..00000000 --- a/docs/docs/typescript-sdk/records/create-records.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create Records - -Three methods for writing flat records. For nested/graph data see [Import Data](./import-data.md). - -## Create a Record - -`db.records.create()` - -```typescript -const movie = await db.records.create({ - label: "MOVIE", - data: { title: "Inception", rating: 8.8, genre: "sci-fi" }, -}); -// → DBRecordInstance { __id, __label, title, rating, genre } -``` - -### Precise type control (PropertyDraft) - -```typescript -await db.records.create({ - label: "MOVIE", - data: [ - { name: "title", type: "string", value: "Inception" }, - { name: "rating", type: "number", value: 8.8 }, - { - name: "genres", - type: "string", - value: "sci-fi,thriller", - valueSeparator: ",", - }, - { name: "releasedAt", type: "datetime", value: "2010-07-16T00:00:00Z" }, - ], -}); -``` - -| PropertyDraft field | Type | Description | -| ------------------- | -------- | ---------------------------------------------------------------- | -| `name` | `string` | Property name | -| `type` | `string` | `string` · `number` · `boolean` · `datetime` · `null` · `vector` | -| `value` | any | The value | -| `valueSeparator` | `string` | Split `value` string into an array on this separator | - -## Create Multiple Records - -`db.records.createMany()` - -Flat rows only — no nested objects. For nested data use [`importJson`](./import-data.md). - -```typescript -const result = await db.records.createMany({ - label: "ACTOR", - data: [ - { name: "Leonardo DiCaprio", country: "USA" }, - { name: "Ken Watanabe", country: "Japan" }, - ], -}); -// → DBRecordsArrayInstance { data: [...], total: 2 } -``` - -## Upsert - -`db.records.upsert()` - -Create-or-update based on matching criteria. - -```typescript -// Match on 'title'; update rating if found, create if not -const movie = await db.records.upsert({ - label: "MOVIE", - data: { title: "Inception", rating: 9.0, genre: "sci-fi" }, - options: { mergeBy: ["title"], mergeStrategy: "append" }, -}); -``` - -### Merge strategies - -| Strategy | Behaviour | -| ------------------ | --------------------------------------------------------------------- | -| `append` (default) | Add / update incoming fields; preserve all other existing fields | -| `rewrite` | Replace all fields with incoming data; unmentioned fields are removed | - -### `mergeBy` behaviour - -| `mergeBy` value | Match behaviour | -| --------------- | ----------------------------------- | -| `['field']` | Match only on listed fields | -| `[]` or omitted | Match on ALL incoming property keys | - -## Options - -All three methods accept the same `options` object: - -| Option | Default | Description | -| ------------------------------- | ------------------------------- | --------------------------------------- | -| `suggestTypes` | `true` | Infer types automatically | -| `convertNumericValuesToNumbers` | `false` | Convert string numbers to number type | -| `capitalizeLabels` | `false` | Uppercase all inferred label names | -| `relationshipType` | `__RUSHDB__RELATION__DEFAULT__` | Relationship type used for nested links | -| `returnResult` | `false` | Return created records in the response | -| `mergeBy` | — | Fields to match on for upsert | -| `mergeStrategy` | `append` | `append` or `rewrite` | - -## In a transaction - -```typescript -const tx = await db.tx.begin(); -try { - const movie = await db.records.create( - { label: "MOVIE", data: { title: "Dune" } }, - tx, - ); - const actor = await db.records.create( - { label: "ACTOR", data: { name: "Timothée Chalamet" } }, - tx, - ); - await db.records.attach( - { source: movie, target: actor, options: { type: "STARS" } }, - tx, - ); - await tx.commit(); -} catch (e) { - await tx.rollback(); - throw e; -} -``` - -## Via Model - -```typescript -const MovieModel = new Model("MOVIE", { - title: { type: "string" }, - rating: { type: "number" }, -}); - -const movie = await MovieModel.create({ title: "Inception", rating: 8.8 }); -const movies = await MovieModel.createMany([ - { title: "Dune" }, - { title: "Arrival" }, -]); -``` - -## See also - -- [Writing Records with Vectors](../ai/write-with-vectors.md) — attach embedding vectors when creating or upserting records diff --git a/docs/docs/typescript-sdk/records/delete-records.md b/docs/docs/typescript-sdk/records/delete-records.md deleted file mode 100644 index fa35543c..00000000 --- a/docs/docs/typescript-sdk/records/delete-records.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Delete Records - -## Delete by ID - -`db.records.deleteById()` - -```typescript -// Single record -await db.records.deleteById("movie-id-123"); - -// Multiple records -await db.records.deleteById(["id-1", "id-2", "id-3"]); -``` - -All relationships attached to deleted records are removed automatically. - -## Bulk Delete - -`db.records.delete()` - -Delete all records matching a search query. - -```typescript -// Delete all sci-fi movies with low ratings -await db.records.delete({ - labels: ["MOVIE"], - where: { genre: "sci-fi", rating: { $lt: 5 } }, -}); -``` - -:::warning -An empty `where` without `allowForceDelete: true` in the SDK config throws `EmptyTargetError`. -::: - -## In a transaction - -```typescript -const tx = await db.tx.begin(); -try { - await db.records.deleteById("movie-id-123", tx); - await db.records.delete( - { labels: ["ACTOR"], where: { country: "temp" } }, - tx, - ); - await tx.commit(); -} catch (e) { - await tx.rollback(); - throw e; -} -``` - -## Via Model - -```typescript -const MovieModel = new Model("MOVIE", { title: { type: "string" } }); - -await MovieModel.deleteById(["id-1", "id-2"]); -await MovieModel.delete({ where: { genre: "temp" } }); -``` diff --git a/docs/docs/typescript-sdk/records/get-records.md b/docs/docs/typescript-sdk/records/get-records.md deleted file mode 100644 index 663489fa..00000000 --- a/docs/docs/typescript-sdk/records/get-records.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Get Records - -RushDB provides four read methods: look up by ID, find one, find unique, or run a full search query. - -## Find by ID - -`db.records.findById()` - -```typescript -const movie = await db.records.findById("movie-id-123"); -const movies = await db.records.findById(["id-1", "id-2", "id-3"]); -``` - -Returns `DBRecordInstance` (single) or `DBRecordsArrayInstance` (array). - -## Find One - -`db.records.findOne()` - -Returns the first matching record, or `null` if none found. - -```typescript -const movie = await db.records.findOne({ - labels: ["MOVIE"], - where: { title: "Inception" }, -}); -``` - -## Find Unique - -`db.records.findUniq()` - -Like `findOne` but throws `NonUniqueResultError` if more than one record matches. - -```typescript -import { NonUniqueResultError } from "@rushdb/javascript-sdk"; - -try { - const movie = await db.records.findUniq({ - labels: ["MOVIE"], - where: { title: "Inception" }, - }); -} catch (e) { - if (e instanceof NonUniqueResultError) - console.error(`found ${e.count} matches`); -} -``` - -## Find Records - -`db.records.find()` - -Full search with filtering, sorting, and pagination. - -```typescript -const { data: movies, total } = await db.records.find({ - labels: ["MOVIE"], - where: { rating: { $gte: 8 }, genre: "sci-fi" }, - orderBy: { rating: "desc" }, - limit: 20, - skip: 0, -}); -``` - -### SearchQuery parameters - -| Field | Type | Description | -| ----------- | ------------------ | -------------------------------------------------------------- | -| `labels` | `string[]` | Filter by one or more labels | -| `where` | `object` | Filter conditions ([docs](../../concepts/search/where)) | -| `orderBy` | `string \| object` | Sort criteria ([docs](../../concepts/search/pagination-order)) | -| `limit` | `number` | Max records to return (default: 1000) | -| `skip` | `number` | Records to skip for pagination | -| `select` | `object` | Output-shaping expressions ([docs](../../concepts/search/select)) | -| `groupBy` | `string[]` | Grouping keys, e.g. `['$record.genre']` | - -## Relationship traversal - -Filter across graph edges inline with `where`: - -```typescript -// Movies where at least one actor is from the USA -const { data } = await db.records.find({ - labels: ["MOVIE"], - where: { - ACTOR: { country: "USA" }, - }, -}); - -// With explicit relation type and direction -const { data: films } = await db.records.find({ - labels: ["MOVIE"], - where: { - DIRECTOR: { - $relation: { type: "DIRECTED_BY", direction: "out" }, - name: { $contains: "Nolan" }, - }, - }, -}); -``` - -See [Where clause docs](../../concepts/search/where#relationship-queries) for full syntax. - -## Field operators - -```typescript -// Numeric range -where: { rating: { $gte: 8, $lte: 9.5 } } - -// Set membership -where: { genre: { $in: ['sci-fi', 'thriller'] } } - -// Text -where: { title: { $contains: 'dark' } } - -// Existence / type checks -where: { $and: [{ poster: { $exists: true } }, { rating: { $type: 'number' } }] } - -// Logical -where: { $or: [{ genre: 'sci-fi' }, { rating: { $gte: 9 } }] } -``` - -Full operator reference: [Where clause docs](../../concepts/search/where). - -## Select Expressions - -```typescript -const stats = await db.records.find({ - labels: ["MOVIE"], - where: { ACTOR: { $alias: "$actor", country: "USA" } }, - select: { - title: "$record.title", - actorCount: { $count: "$actor" }, - avgRating: { $avg: "$record.rating", $precision: 1 }, - actorNames: { $collect: { from: "$actor", select: { name: "$actor.name" } } }, - }, -}); -``` - -:::danger Do not set `limit` when using `select` — it cuts the scan and returns mathematically incorrect totals. Use `orderBy` on a `select` output key instead. -::: - -### GroupBy - -```typescript -const byGenre = await db.records.find({ - labels: ["MOVIE"], - select: { - count: { $count: "*" }, - avgRating: { $avg: "$record.rating", $precision: 1 }, - }, - groupBy: ["$record.genre"], - orderBy: { count: "desc" }, -}); -// [{ genre: 'sci-fi', count: 42, avgRating: 7.9 }, ...] -``` - -Full reference: [Select Expressions](../../concepts/search/select) · [Grouping](../../concepts/search/group-by) - -## TimeBucket (time-series) - -```typescript -const daily = await db.records.find({ - labels: ["ORDER"], - select: { - day: { $timeBucket: { field: "$record.createdAt", unit: "day" } }, - count: { $count: "*" }, - }, - groupBy: ["day"], - orderBy: { day: "asc" }, -}); -``` - -`unit` values: `day` · `week` · `month` · `quarter` · `year` · `hours` · `minutes` · `seconds` (use plural + `size` for custom window widths). - -## Collect related records - -Label-based (no alias needed — preferred for nesting): - -```typescript -const companies = await db.records.find({ - labels: ["COMPANY"], - select: { - name: "$record.name", - departments: { - $collect: { - label: "DEPARTMENT", - select: { - name: "$self.name", - projects: { - $collect: { - label: "PROJECT", - select: { name: "$self.name" } - } - } - } - } - } - } -}); -``` - -Alias-based (requires `$alias` in `where`): - -```typescript -const movies = await db.records.find({ - labels: ["MOVIE"], - where: { ACTOR: { $alias: "$actor" } }, - select: { - title: "$record.title", - actors: { - $collect: { - from: "$actor", - select: { name: "$actor.name", country: "$actor.country" }, - }, - }, - }, -}); -``` - -## In a transaction - -```typescript -const tx = await db.tx.begin(); -try { - const { data } = await db.records.find({ labels: ["MOVIE"] }, tx); - // … do more work … - await tx.commit(); -} catch (e) { - await tx.rollback(); - throw e; -} -``` - -## Via Model - -```typescript -const MovieModel = new Model("MOVIE", { - title: { type: "string" }, - rating: { type: "number" }, -}); - -const all = await MovieModel.find(); -const sciFi = await MovieModel.find({ where: { genre: "sci-fi" } }); -const one = await MovieModel.findOne({ where: { title: "Inception" } }); -const byId = await MovieModel.findById("movie-id-123"); -const unique = await MovieModel.findUniq({ where: { title: "Inception" } }); -``` - -Model search methods auto-fill `labels` from the model definition. diff --git a/docs/docs/typescript-sdk/records/import-data.md b/docs/docs/typescript-sdk/records/import-data.md deleted file mode 100644 index 0bdf5258..00000000 --- a/docs/docs/typescript-sdk/records/import-data.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Import Data - -When importing data into RushDB, choose the method that matches your data shape: - -- createMany — arrays of flat rows only (CSV-like). No nested objects or arrays inside items. -- importJson — real JSON: nested, messy, arrays with nested data, or a hash-map-like top-level structure. - -This page explains when to use each and shows practical examples. Keep using createMany where you already import flat arrays; use importJson for everything else. - -## createMany: arrays of flat rows (CSV-like) + optional upsert -Use createMany when your input is an array (or single object) of flat rows. Nested objects/arrays are not allowed. - -You can also perform an upsert during creation by supplying `options.mergeBy` and/or `options.mergeStrategy`: - -- If `mergeBy` (array) is provided (even empty) OR `mergeStrategy` is provided, RushDB will attempt to match existing records. -- If `mergeBy` is empty or omitted but `mergeStrategy` is present, all incoming property keys are used for matching. -- `mergeStrategy: 'append'` (default) adds/updates incoming properties, preserving unspecified ones. -- `mergeStrategy: 'rewrite'` replaces all existing properties with incoming ones (unmentioned properties are removed). - -```typescript -// Upsert (append) by email -await db.records.createMany({ - label: 'AUTHOR', - data: authors, - options: { mergeBy: ['email'], mergeStrategy: 'append', suggestTypes: true } -}) - -// Rewrite (replace) by email -await db.records.createMany({ - label: 'AUTHOR', - data: authors, - options: { mergeBy: ['email'], mergeStrategy: 'rewrite' } -}) -``` - -```typescript -import RushDB from '@rushdb/javascript-sdk'; -const db = new RushDB(process.env.RUSHDB_API_KEY!); - -const authors = [ - { name: 'Alice Johnson', email: 'alice@example.com', age: 30 }, - { name: 'Bob Smith', email: 'bob@example.com', age: 25 } -]; - -const result = await db.records.createMany({ - label: 'AUTHOR', - data: authors, - options: { suggestTypes: true } -}); - -console.log(result.data.map(r => r.data)); -``` - -## Importing Data from CSV - -Use `importCsv` when your data source is a CSV string. You can control both import options (type inference etc.), upsert behavior, and a subset of CSV parsing configuration. - -```typescript -import RushDB from '@rushdb/javascript-sdk'; - -const db = new RushDB(process.env.RUSHDB_API_KEY!); - -const csv = `name,email,age\nJohn Doe,john@example.com,30\nJane Smith,jane@example.com,25`; - -const customers = await db.records.importCsv({ - label: 'CUSTOMER', - data: csv, - options: { - suggestTypes: true, - convertNumericValuesToNumbers: true, - returnResult: true, - mergeBy: ['email'], // upsert match key(s) - mergeStrategy: 'append' // or 'rewrite' - }, - parseConfig: { - delimiter: ',', - header: true, - skipEmptyLines: true, - dynamicTyping: true - } -}); - -console.log(customers.data.map(c => c.data)); -``` - -### CSV Parse Configuration (parseConfig) - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `delimiter` | `string` | `,` | Field delimiter | -| `header` | `boolean` | `true` | Whether first row contains headers | -| `skipEmptyLines` | `boolean` \| `"greedy"` | `true` | Skip empty (or whitespace-only when `greedy`) lines | -| `dynamicTyping` | `boolean` | Mirrors `options.suggestTypes` | PapaParse numeric/boolean autodetection | -| `quoteChar` | `string` | `"` | Quote character | -| `escapeChar` | `string` | `"` | Escape character for quotes | -| `newline` | `string` | auto | Explicit newline sequence override | - -If `parseConfig.dynamicTyping` is omitted, it inherits from `options.suggestTypes`. - -## importJson: nested or hash-map-like JSON -Use importJson when your data is nested or “messy” (arrays with nested objects, objects-within-objects, etc.), or when your top-level input is a hash map of label -> items. - -importJson works in two modes: - -1) With label provided — you explicitly set the top-level label. - -```typescript -import RushDB from '@rushdb/javascript-sdk'; -import fs from 'fs'; - -const db = new RushDB(process.env.RUSHDB_API_KEY!); -const data = JSON.parse(fs.readFileSync('data.json', 'utf8')); - -// data.json can be nested/messy. importJson will BFS through and create -// records and relationships according to structure. -const imported = await db.records.importJson({ - label: 'BLOG', - data, - options: { suggestTypes: true } -}); - -console.log(imported.data.length); -``` - -2) Without label — pass a single top-level key used as the label: - -```typescript -const payload = { - ITEM: [ - { name: 'Sprocket', specs: { size: 'M', weight: 1.2 }, images: ['a.jpg', 'b.jpg'] }, - { name: 'Cog', specs: { size: 'S', weight: 0.7 } } - ] -} - -// Label is inferred as 'ITEM' -await db.records.importJson({ data: payload }); -``` - -Unlabeled invalid root object — if you don’t provide label and the top level is not a single-key map, importJson throws: - -```typescript -await db.records.importJson({ - data: { - some: 'key', - data: 1, - nested: { level: 2 } - } -}); -// Error: importJson requires either an explicit label or a single top-level key to infer the label -``` - -### Advanced Usage: Import Options - -The `importJson` method accepts an optional `options` parameter to customize how your data is processed and stored: - -```typescript -const importOptions = { - suggestTypes: true, - convertNumericValuesToNumbers: true, - capitalizeLabels: false, - relationshipType: 'OWNS', - returnResult: true -}; - -const importedUsers = await db.records.importJson({ label: 'user', data: data.users, options: importOptions }) -``` - -### Available Options (JSON, CSV & createMany) - -| Option | Type | Default | Description | -|---------------------------------|---------|---------------------------------|---------------------------------------------------| -| `suggestTypes` | Boolean | `true` | **Default is `true`** - Automatically infers data types for properties. Set to `false` to disable type inference and store all values as strings | -| `convertNumericValuesToNumbers` | Boolean | `false` | Converts string numbers to number type | -| `capitalizeLabels` | Boolean | `false` | Converts all labels to uppercase | -| `relationshipType` | String | `__RUSHDB__RELATION__DEFAULT__` | Default relationship type between Records (nodes) | -| `returnResult` | Boolean | `false` | Returns imported records in response | -| `mergeBy` | String[] | `undefined` / `[]` | Property names used to locate existing records. If omitted and `mergeStrategy` provided, all incoming property keys are used. Empty array means “use all incoming keys”. | -| `mergeStrategy` | `'append' \| 'rewrite'` | `'append'` | Upsert behavior: append updates/adds properties; rewrite replaces all existing properties. Providing either option triggers upsert semantics. | - -:::info Default Type Inference -By default, `suggestTypes` is `true` for all import operations (importJson, importCsv, createMany). RushDB automatically infers data types from your values. To disable this and store all properties as strings, explicitly set `suggestTypes: false`. -::: - -### Upsert Matching Rules Recap - -- Triggered when `mergeStrategy` or `mergeBy` is present in `options`. -- If `mergeBy` omitted and `mergeStrategy` supplied → all incoming keys form the match fingerprint. -- If `mergeBy` is an empty array (`[]`) → also treated as “all incoming keys”. -- `append` preserves unspecified properties; `rewrite` removes them. - -## Quick rules recap - -- createMany: arrays of flat rows only. Nested objects or arrays inside items are not allowed and will cause an error — use importJson instead. -- importJson: nested/mixed JSON. Provide label explicitly, or pass a single-key object like `{ LABEL: [...] }` to infer the label. -- importCsv: CSV string input with parseConfig; dynamicTyping inherits from options.suggestTypes when omitted. - -## See also - -- [Writing Records with Vectors](../ai/write-with-vectors.md) — attach pre-computed embedding vectors to records at write time - diff --git a/docs/docs/typescript-sdk/records/update-records.md b/docs/docs/typescript-sdk/records/update-records.md deleted file mode 100644 index d731a473..00000000 --- a/docs/docs/typescript-sdk/records/update-records.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Update Records - -Two methods for updating records: `update()` patches fields, `set()` replaces them all. - -## Partial Update - -`db.records.update()` - -Partial update — only the specified fields change; all other fields are preserved. - -```typescript -await db.records.update({ - target: "movie-id-123", - label: "MOVIE", - data: { rating: 9.1 }, -}); -// → DBRecordInstance (title, genre, etc. unchanged) -``` - -## Full Replacement - -`db.records.set()` - -Full replace — all fields not in `data` are removed. - -```typescript -await db.records.set({ - target: "movie-id-123", - label: "MOVIE", - data: { title: "Inception", rating: 9.1, genre: "sci-fi" }, -}); -// → DBRecordInstance (only these three fields remain) -``` - -### Parameters (both methods) - -| Parameter | Description | -| ------------- | --------------------------------------------------------- | -| `target` | Record ID string, record instance, or record object | -| `label` | Label for the record | -| `data` | Flat object or `PropertyDraft[]` for precise type control | -| `options` | `suggestTypes`, `convertNumericValuesToNumbers` | -| `transaction` | Optional `Transaction` or ID string | - -### PropertyDraft approach - -```typescript -await db.records.update({ - target: "movie-id-123", - label: "MOVIE", - data: [ - { name: "rating", type: "number", value: 9.1 }, - { name: "releasedAt", type: "datetime", value: "2010-07-16T00:00:00Z" }, - ], -}); -``` - -## In a transaction - -```typescript -const tx = await db.tx.begin(); -try { - await db.records.update( - { target: "movie-id-123", label: "MOVIE", data: { rating: 9.1 } }, - tx, - ); - await db.records.update( - { target: "actor-id-456", label: "ACTOR", data: { country: "USA" } }, - tx, - ); - await tx.commit(); -} catch (e) { - await tx.rollback(); - throw e; -} -``` - -## Via Model - -```typescript -const MovieModel = new Model("MOVIE", { - title: { type: "string" }, - rating: { type: "number" }, -}); - -// Partial update -await MovieModel.update("movie-id-123", { rating: 9.1 }); - -// Full replace -await MovieModel.set("movie-id-123", { - title: "Inception", - rating: 9.1, - genre: "sci-fi", -}); -``` - -## See also - -- [Writing Records with Vectors](../ai/write-with-vectors.md) — attach embedding vectors when creating or upserting records diff --git a/docs/docs/typescript-sdk/relationships.md b/docs/docs/typescript-sdk/relationships.md deleted file mode 100644 index 372fabfc..00000000 --- a/docs/docs/typescript-sdk/relationships.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Relationships - -Connect records into a graph. Relationships have a type and a direction. - -## Attach - -`db.records.attach()` - -```typescript -await db.records.attach({ - source: "movie-id-123", - target: "actor-id-456", - options: { type: "STARS", direction: "out" }, - // (MOVIE) -[:STARS]-> (ACTOR) -}); -``` - -`target` can be a single ID, an array of IDs, or a record instance. - -## Detach - -`db.records.detach()` - -```typescript -await db.records.detach({ - source: "movie-id-123", - target: "actor-id-456", - options: { typeOrTypes: "STARS" }, // omit to detach all types -}); -``` - -## Bulk create by key match - -Connect records by matching a property on the source to a property on the target: - -```typescript -await db.relationships.createMany({ - source: { label: "MOVIE", key: "directorId" }, - target: { label: "DIRECTOR", key: "id" }, - type: "DIRECTED_BY", - direction: "out", -}); -// Creates MOVIE -[:DIRECTED_BY]-> DIRECTOR where MOVIE.directorId = DIRECTOR.id -``` - -## Bulk delete by key match - -```typescript -await db.relationships.deleteMany({ - source: { label: "MOVIE", key: "directorId" }, - target: { label: "DIRECTOR", key: "id" }, - type: "DIRECTED_BY", - direction: "out", -}); -``` - -### Many-to-many deletion (cartesian) - -```typescript -// Deletes HAS_TAG relationships between ALL matching pairs -await db.relationships.deleteMany({ - source: { label: "MOVIE", where: { genre: "sci-fi" } }, - target: { label: "TAG", where: { category: "genre" } }, - type: "HAS_TAG", - manyToMany: true, // Must be explicit — requires non-empty where on both sides -}); -``` - -:::warning Both `source.where` and `target.where` must be non-empty when `manyToMany: true`. -::: - -## Find relationships - -```typescript -const { data, total } = await db.relationships.find({ - labels: ["MOVIE"], - where: { - ACTOR: { $relation: "STARS", country: "USA" }, - }, -}); -``` - -## In a transaction - -```typescript -const tx = await db.tx.begin(); -try { - const movie = await db.records.create( - { label: "MOVIE", data: { title: "Dune" } }, - tx, - ); - const actor = await db.records.create( - { label: "ACTOR", data: { name: "Timothée Chalamet" } }, - tx, - ); - await db.records.attach( - { source: movie, target: actor, options: { type: "STARS" } }, - tx, - ); - await tx.commit(); -} catch (e) { - await tx.rollback(); - throw e; -} -``` - -## Via Model - -```typescript -await MovieModel.attach({ - source: "movie-id-123", - target: "actor-id-456", - options: { type: "STARS", direction: "out" }, -}); - -await MovieModel.detach({ - source: "movie-id-123", - target: "actor-id-456", - options: { typeOrTypes: "STARS" }, -}); -``` - -## Direction reference - -| `direction` | Graph pattern | -| ----------- | ------------------------------ | -| `out` | `(source) -[:TYPE]-> (target)` | -| `in` | `(source) <-[:TYPE]- (target)` | diff --git a/docs/docs/typescript-sdk/transactions.md b/docs/docs/typescript-sdk/transactions.md deleted file mode 100644 index a68a84c8..00000000 --- a/docs/docs/typescript-sdk/transactions.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Transactions - -Group multiple operations into an atomic unit — all succeed or all roll back. - -## Basic pattern - -```typescript -const tx = await db.tx.begin() -// optional: { ttl: 10000 } — default 5000ms, max 30000ms - -try { - const movie = await db.records.create( - { label: 'MOVIE', data: { title: 'Dune', rating: 8.0 } }, tx) - - const actor = await db.records.create( - { label: 'ACTOR', data: { name: 'Timothée Chalamet' } }, tx) - - await db.records.attach( - { source: movie, target: actor, options: { type: 'STARS' } }, tx) - - await tx.commit() // or: await db.tx.commit(tx) -} catch (e) { - await tx.rollback() // or: await db.tx.rollback(tx) - throw e -} -``` - -Pass `tx` as the last argument to any record/relationship method. - -## API - -| Method | Description | -|---|---| -| `db.tx.begin({ ttl? })` | Start a transaction. Returns a `Transaction` object with `.id` | -| `db.tx.get(tx)` | Check if a transaction still exists | -| `db.tx.commit(tx)` | Commit — makes all changes permanent | -| `db.tx.rollback(tx)` | Rollback — discards all changes | -| `tx.commit()` / `tx.rollback()` | Shorthand on the transaction object itself | - -## Timeouts - -Uncommitted transactions auto-rollback after the TTL expires. - -| Parameter | Value | -|---|---| -| Default TTL | 5 000 ms | -| Maximum TTL | 30 000 ms | - type: "LIVES_AT", diff --git a/docs/docs/typescript-sdk/typescript-reference/_category_.json b/docs/docs/typescript-sdk/typescript-reference/_category_.json deleted file mode 100644 index 317450d4..00000000 --- a/docs/docs/typescript-sdk/typescript-reference/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "Typescript Reference", - "position": 9, - "collapsed": true, - "collapsible": true -} diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index a66eb691..f2f009bf 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -4,6 +4,8 @@ import tailwindPlugin from './plugins/tailwind-config.cjs' import * as path from 'node:path' import * as fs from 'node:fs' +const CopyWebpackPlugin = require('copy-webpack-plugin') + // Helper function to generate categorized content files async function generateCategorizedContentFiles( llmsDir: string, @@ -13,30 +15,24 @@ async function generateCategorizedContentFiles( ) { // Create mapping from file content to routes to categorize the full content const categories = { - concepts: [] as string[], - 'typescript-sdk': [] as string[], - 'python-sdk': [] as string[], - 'rest-api': [] as string[], - 'mcp-server': [] as string[], - tutorials: [] as string[], + learn: [] as string[], + connect: [] as string[], + deploy: [] as string[], + 'rushdb-cloud': [] as string[], 'get-started': [] as string[] } // Group content by file paths directly for (const [filePath, content] of filePathToContent.entries()) { // Categorize by file path prefix - if (filePath.startsWith('concepts/')) { - categories.concepts.push(content) - } else if (filePath.startsWith('typescript-sdk/')) { - categories['typescript-sdk'].push(content) - } else if (filePath.startsWith('python-sdk/')) { - categories['python-sdk'].push(content) - } else if (filePath.startsWith('rest-api/')) { - categories['rest-api'].push(content) - } else if (filePath.startsWith('mcp-server/')) { - categories['mcp-server'].push(content) - } else if (filePath.startsWith('tutorials/')) { - categories.tutorials.push(content) + if (filePath.startsWith('learn/')) { + categories.learn.push(content) + } else if (filePath.startsWith('connect/')) { + categories.connect.push(content) + } else if (filePath.startsWith('deploy/')) { + categories.deploy.push(content) + } else if (filePath.startsWith('rushdb-cloud/')) { + categories['rushdb-cloud'].push(content) } else if (filePath.startsWith('get-started/')) { categories['get-started'].push(content) } @@ -162,6 +158,10 @@ const config: Config = { // For GitHub pages deployment, it is often '//' baseUrl: '/', + // Serve raw .mdx/.md source files at their natural paths (e.g. /deploy/infra-neo4j.mdx) + // so CopyPageButton can fetch content locally without depending on GitHub raw. + staticDirectories: ['static', 'docs'], + onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', @@ -176,25 +176,135 @@ const config: Config = { plugins: [ tailwindPlugin, require('./plugins/tutorials-data.cjs'), + async function pluginRawDocs(context) { + return { + name: 'raw-docs-plugin', + configureWebpack(_config, isServer) { + if (isServer) return {} + + return { + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join(context.siteDir, 'docs'), + to: 'raw-docs', + globOptions: { + ignore: ['**/_category_.json'] + } + } + ] + }) + ] + } + } + } + }, [ '@docusaurus/plugin-client-redirects', { redirects: [ - // Old basic-concepts/ → concepts/ - { from: '/basic-concepts/properties', to: '/concepts/properties' }, - { from: '/basic-concepts/records', to: '/concepts/records' }, - { from: '/basic-concepts/relations', to: '/concepts/relationships' }, - { from: '/basic-concepts/transactions', to: '/concepts/transactions' }, - // Old advanced/ → concepts/ - { from: '/advanced/properties', to: '/concepts/properties' }, - { from: '/advanced/data-types', to: '/concepts/properties' }, - { from: '/advanced/records', to: '/concepts/records' }, - { from: '/advanced/relationships', to: '/concepts/relationships' }, - { from: '/advanced/querying-data', to: '/concepts/search/introduction' }, - { from: '/advanced/search-aggregation', to: '/concepts/search/introduction' }, - { from: '/advanced/enhanced-typescript', to: '/typescript-sdk/introduction' }, - // Old python-sdk/records-api → python-sdk/records/ - { from: '/python-sdk/records-api', to: '/python-sdk/records/create-records' } + { from: '/', to: '/get-started/quick-tutorial' }, + // Legacy concepts now live in the curated Learn sections. + { from: '/basic-concepts/properties', to: '/build/schema/labels-and-properties' }, + { from: '/basic-concepts/records', to: '/build/data/store-records' }, + { from: '/basic-concepts/relations', to: '/build/graph/' }, + { from: '/basic-concepts/transactions', to: '/build/reliability/transactions' }, + { from: '/advanced/properties', to: '/build/schema/labels-and-properties' }, + { from: '/advanced/data-types', to: '/build/data/labeled-meta-property-graph' }, + { from: '/advanced/records', to: '/build/data/store-records' }, + { from: '/advanced/relationships', to: '/build/graph/' }, + { from: '/advanced/querying-data', to: '/reference/search-query' }, + { from: '/advanced/search-aggregation', to: '/reference/select-expressions' }, + { from: '/advanced/enhanced-typescript', to: '/reference/typescript/' }, + { from: '/concepts', to: '/build/data/labeled-meta-property-graph' }, + { from: '/concepts/bring-your-own-vectors', to: '/build/ai-search/bring-your-own-vectors' }, + { from: '/concepts/data-ingestion', to: '/build/data/import-data' }, + { from: '/concepts/labels', to: '/build/schema/labels-and-properties' }, + { from: '/concepts/properties', to: '/build/schema/labels-and-properties' }, + { from: '/concepts/records', to: '/build/data/store-records' }, + { from: '/concepts/relationships', to: '/build/graph/' }, + { from: '/concepts/search', to: '/reference/search-query' }, + { from: '/concepts/search/introduction', to: '/reference/search-query' }, + { from: '/concepts/search/labels', to: '/reference/search-labels' }, + { from: '/concepts/search/pagination-order', to: '/reference/pagination-order' }, + { from: '/concepts/search/select', to: '/reference/select-expressions' }, + { from: '/concepts/search/group-by', to: '/reference/group-by' }, + { from: '/concepts/search/where', to: '/reference/where-operators' }, + { from: '/concepts/semantic-search', to: '/build/ai-search/semantic-search' }, + { from: '/concepts/storage', to: '/build/data/labeled-meta-property-graph' }, + { from: '/concepts/transactions', to: '/build/reliability/transactions' }, + { from: '/concepts/billing-model', to: '/cloud/billing-model' }, + { from: '/concepts/knowledge-units', to: '/cloud/knowledge-units' }, + { from: '/pricing-and-billing/billing-model', to: '/cloud/billing-model' }, + { from: '/pricing-and-billing/knowledge-units', to: '/cloud/knowledge-units' }, + { from: '/build/models', to: '/reference/typescript/Model' }, + { from: '/deploy/local-setup', to: '/deploy/local-docker' }, + { from: '/deploy/dashboard-configuration', to: '/deploy/get-api-key' }, + { from: '/get-started/get-api-key', to: '/deploy/get-api-key' }, + // Moved deployment tutorials. + { from: '/tutorials/configuring-dashboard', to: '/deploy/get-api-key' }, + { from: '/tutorials/connect-aura-instance', to: '/deploy/connect-aura' }, + { from: '/tutorials/deployment', to: '/deploy/docker-remote' }, + { from: '/tutorials/local-setup', to: '/deploy/local-docker' }, + { from: '/tutorials/mcp-operator-quickstart', to: '/deploy/mcp-operator-quickstart' }, + { from: '/tutorials/self-hosted-project-setup', to: '/deploy/self-hosted-project-setup' }, + // The standalone MCP guide has been consolidated into Connect. + { from: '/mcp-server/introduction', to: '/connect/mcp' }, + { from: '/mcp-server/quickstart', to: '/connect/mcp' }, + { from: '/mcp-server/configuration', to: '/connect/mcp' }, + { from: '/mcp-server/tools', to: '/connect/mcp' }, + { from: '/mcp-server/examples', to: '/connect/mcp' }, + { from: '/mcp-server/troubleshooting', to: '/connect/mcp' }, + // SDK guides are represented by Learn workflows and generated references. + { from: '/typescript-sdk/introduction', to: '/reference/typescript/' }, + { from: '/typescript-sdk/labels', to: '/build/schema/labels-and-properties' }, + { from: '/typescript-sdk/properties', to: '/build/schema/labels-and-properties' }, + { from: '/typescript-sdk/models', to: '/reference/typescript/Model' }, + { from: '/typescript-sdk/raw-queries', to: '/build/raw-queries' }, + { from: '/typescript-sdk/relationships', to: '/build/graph/connect-records' }, + { from: '/typescript-sdk/transactions', to: '/build/reliability/transactions' }, + { from: '/typescript-sdk/records/create-records', to: '/build/data/store-records' }, + { from: '/typescript-sdk/records/get-records', to: '/build/data/find-and-query' }, + { from: '/typescript-sdk/records/update-records', to: '/build/data/store-records' }, + { from: '/typescript-sdk/records/delete-records', to: '/build/data/store-records' }, + { from: '/typescript-sdk/records/import-data', to: '/build/data/import-data' }, + { from: '/typescript-sdk/ai/overview', to: '/build/ai-search/semantic-search' }, + { from: '/typescript-sdk/ai/indexing', to: '/build/ai-search/manage-indexes' }, + { from: '/typescript-sdk/ai/advanced-indexing', to: '/build/ai-search/bring-your-own-vectors' }, + { from: '/typescript-sdk/ai/search', to: '/build/ai-search/semantic-search' }, + { from: '/typescript-sdk/ai/write-with-vectors', to: '/build/ai-search/write-with-vectors' }, + { from: '/typescript-sdk/typescript-reference/DBRecord', to: '/reference/typescript/DBRecord' }, + { from: '/typescript-sdk/typescript-reference/DBRecordInstance', to: '/reference/typescript/DBRecordInstance' }, + { from: '/typescript-sdk/typescript-reference/DBRecordTarget', to: '/reference/typescript/DBRecordTarget' }, + { from: '/typescript-sdk/typescript-reference/DBRecordsArrayInstance', to: '/reference/typescript/DBRecordsArrayInstance' }, + { from: '/typescript-sdk/typescript-reference/Model', to: '/reference/typescript/Model' }, + { from: '/typescript-sdk/typescript-reference/RelationTarget', to: '/reference/typescript/RelationTarget' }, + { from: '/typescript-sdk/typescript-reference/RushDB', to: '/reference/typescript/RushDB' }, + { from: '/typescript-sdk/typescript-reference/SearchQuery', to: '/reference/typescript/SearchQuery' }, + { from: '/typescript-sdk/typescript-reference/Transaction', to: '/reference/typescript/Transaction' }, + { from: '/python-sdk/introduction', to: '/reference/python/' }, + { from: '/python-sdk/labels', to: '/build/schema/labels-and-properties' }, + { from: '/python-sdk/properties', to: '/build/schema/labels-and-properties' }, + { from: '/python-sdk/raw-queries', to: '/build/raw-queries' }, + { from: '/python-sdk/relationships', to: '/build/graph/connect-records' }, + { from: '/python-sdk/transactions', to: '/build/reliability/transactions' }, + { from: '/python-sdk/records-api', to: '/build/data/store-records' }, + { from: '/python-sdk/records/create-records', to: '/build/data/store-records' }, + { from: '/python-sdk/records/get-records', to: '/build/data/find-and-query' }, + { from: '/python-sdk/records/update-records', to: '/build/data/store-records' }, + { from: '/python-sdk/records/delete-records', to: '/build/data/store-records' }, + { from: '/python-sdk/records/import-data', to: '/build/data/import-data' }, + { from: '/python-sdk/ai/overview', to: '/build/ai-search/semantic-search' }, + { from: '/python-sdk/ai/indexing', to: '/build/ai-search/manage-indexes' }, + { from: '/python-sdk/ai/advanced-indexing', to: '/build/ai-search/bring-your-own-vectors' }, + { from: '/python-sdk/ai/search', to: '/build/ai-search/semantic-search' }, + { from: '/python-sdk/ai/write-with-vectors', to: '/build/ai-search/write-with-vectors' }, + { from: '/python-sdk/python-reference/record', to: '/reference/python/record' }, + { from: '/python-sdk/python-reference/RushDB', to: '/reference/python/RushDB' }, + { from: '/python-sdk/python-reference/search-result', to: '/reference/python/search-result' }, + { from: '/python-sdk/python-reference/SearchQuery', to: '/reference/python/SearchQuery' }, + { from: '/python-sdk/python-reference/transaction', to: '/reference/python/transaction' } ] } ], @@ -268,36 +378,55 @@ const config: Config = { // Categorize docs by section const categories = { - concepts: [] as string[], - 'typescript-sdk': [] as string[], - 'python-sdk': [] as string[], - 'rest-api': [] as string[], - 'mcp-server': [] as string[], - tutorials: [] as string[], + learn: [] as string[], + connect: [] as string[], + deploy: [] as string[], + 'rushdb-cloud': [] as string[], 'get-started': [] as string[] } + const docPermalinks = new Map() + try { + const globalDataPath = path.join(context.generatedFilesDir, 'globalData.json') + const globalData = JSON.parse(await fs.promises.readFile(globalDataPath, 'utf8')) as { + 'docusaurus-plugin-content-docs'?: { + default?: { + versions?: Array<{ + name?: string + docs?: Array<{ id?: string; path?: string }> + }> + } + } + } + const versions = globalData['docusaurus-plugin-content-docs']?.default?.versions ?? [] + const currentVersion = versions.find((version) => version.name === 'current') ?? versions[0] + currentVersion?.docs?.forEach((doc) => { + if (doc.id && doc.path) { + docPermalinks.set(doc.id, doc.path) + } + }) + } catch (err) { + console.warn('Unable to load Docusaurus permalinks for llms.txt; falling back to doc IDs.', err) + } + // Group all docs records by category and prepare content maps const contentByPath = new Map() const docsRecords: string[] = [] Object.entries(currentVersionDocsRoutes).forEach(([path, record]) => { - const docEntry = `- [${record.title}](https://docs.rushdb.com/${path}): ${record.description}` + const permalink = docPermalinks.get(path) ?? `/${path}` + const docEntry = `- [${record.title}](https://docs.rushdb.com${permalink}): ${record.description}` docsRecords.push(docEntry) // Categorize by path prefix - if (path.startsWith('concepts/')) { - categories.concepts.push(docEntry) - } else if (path.startsWith('typescript-sdk/')) { - categories['typescript-sdk'].push(docEntry) - } else if (path.startsWith('python-sdk/')) { - categories['python-sdk'].push(docEntry) - } else if (path.startsWith('rest-api/')) { - categories['rest-api'].push(docEntry) - } else if (path.startsWith('mcp-server/')) { - categories['mcp-server'].push(docEntry) - } else if (path.startsWith('tutorials/')) { - categories.tutorials.push(docEntry) + if (path.startsWith('learn/')) { + categories.learn.push(docEntry) + } else if (path.startsWith('connect/')) { + categories.connect.push(docEntry) + } else if (path.startsWith('deploy/')) { + categories.deploy.push(docEntry) + } else if (path.startsWith('rushdb-cloud/')) { + categories['rushdb-cloud'].push(docEntry) } else if (path.startsWith('get-started/')) { categories['get-started'].push(docEntry) } @@ -315,12 +444,15 @@ const config: Config = { // Generate isolated txt files for each category for (const [categoryName, categoryDocs] of Object.entries(categories)) { if (categoryDocs.length > 0) { - const categoryTitle = categoryName - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') - - const categoryTxt = `# RushDB ${categoryTitle}\n\n${categoryDocs.join('\n')}` + const categoryTitle = + categoryName === 'rushdb-cloud' + ? 'RushDB Cloud' + : `RushDB ${categoryName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ')}` + + const categoryTxt = `# ${categoryTitle}\n\n${categoryDocs.join('\n')}` const categoryPath = path.join(llmsDir, `llms-${categoryName}.txt`) try { @@ -378,46 +510,17 @@ const config: Config = { }, image: 'img/og.png', navbar: { - title: 'rushdb docs', + title: 'docs', logo: { alt: 'RushDB Logo', src: 'img/logo.svg' }, items: [ { - label: 'Python SDK', - to: '/python-sdk/introduction', - className: 'python-sdk', - position: 'left', - activeBaseRegex: '/python-sdk/' - }, - { - label: 'TypeScript SDK', - to: '/typescript-sdk/introduction', - className: 'typescript-sdk', - position: 'left', - activeBaseRegex: '/typescript-sdk/' - }, - { - label: 'REST API', - to: '/rest-api/introduction', - className: 'rest-api', - position: 'left', - activeBaseRegex: '/rest-api/' - }, - { - label: 'MCP Server', - to: '/mcp-server/introduction', - className: 'mcp-server', + label: 'Learn', + to: '/build/data/', position: 'left', - activeBaseRegex: '/mcp-server/' - }, - { - type: 'docSidebar', - sidebarId: 'tutorials', - label: 'Tutorials', - position: 'left', - className: 'tutorials-link' + activeBaseRegex: '/(build|connect|deploy|reference|rest-api|tutorials)/' }, { href: 'https://github.com/rush-db/rushdb', @@ -439,63 +542,6 @@ const config: Config = { } ] }, - footer: { - links: [ - { - title: 'Documentation', - items: [ - { - label: 'Getting Started', - to: '/get-started/quick-tutorial' - }, - { - label: 'Basic Concepts', - to: '/concepts/records' - } - ] - }, - { - title: 'APIs & Integrations', - items: [ - { - label: 'REST API', - to: '/rest-api/introduction' - }, - { - label: 'Python SDK', - to: '/python-sdk/introduction' - }, - { - label: 'TypeScript SDK', - to: '/typescript-sdk/introduction' - }, - { - label: 'MCP Server', - to: '/mcp-server/introduction' - } - ] - }, - { - title: 'Community', - items: [ - { - label: 'X (Twitter)', - href: 'https://x.com/RushDatabase' - }, - { - label: 'LinkedIn', - href: 'https://www.linkedin.com/company/rushdb' - }, - { - label: 'GitHub', - href: 'https://github.com/rush-db/rushdb' - } - ], - className: 'text-right' - } - ], - copyright: `© ${new Date().getFullYear()}, Collect Software Inc.` - }, prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, diff --git a/docs/plugins/tutorials-data.cjs b/docs/plugins/tutorials-data.cjs index db87ad8f..f0d31a8e 100644 --- a/docs/plugins/tutorials-data.cjs +++ b/docs/plugins/tutorials-data.cjs @@ -39,31 +39,37 @@ function estimateReadTime(content) { return `${Math.max(1, minutes)} min` } +function getTutorialFiles(dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const filePath = path.join(dir, entry.name) + if (entry.isDirectory()) return getTutorialFiles(filePath) + return /\.(md|mdx)$/.test(entry.name) ? [filePath] : [] + }) +} + /** * @param {string} siteDir * @returns {Array<{id: string, title: string, description: string, href: string, tags: string[], time: string}>} */ function loadTutorials(siteDir) { - const tutorialsDir = path.join(siteDir, 'docs', 'tutorials') - const entries = fs.readdirSync(tutorialsDir) + const tutorialsDir = path.join(siteDir, 'docs', 'learn', 'tutorials') + const tutorialFiles = getTutorialFiles(tutorialsDir) const tutorials = [] - for (const entry of entries) { - if (!/\.(md|mdx)$/.test(entry)) continue - if (entry === 'index.mdx') continue + for (const filePath of tutorialFiles) { + if (path.basename(filePath) === 'index.mdx') continue - const filePath = path.join(tutorialsDir, entry) const raw = fs.readFileSync(filePath, 'utf8') const { data: fm, content } = matter(raw) - const slug = entry.replace(/\.(md|mdx)$/, '') + const slug = path.basename(filePath).replace(/\.(md|mdx)$/, '') tutorials.push({ id: slug, title: fm.title || slug, description: fm.description || '', - href: `/tutorials/${slug}`, + href: fm.slug || `/tutorials/${slug}`, tags: Array.isArray(fm.tags) ? fm.tags : [], time: estimateReadTime(content), sidebar_position: fm.sidebar_position ?? 999 diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 6bcfe71b..65e2d7e5 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -1,74 +1,375 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs' const sidebars: SidebarsConfig = { - restApi: [{ type: 'autogenerated', dirName: 'rest-api' }], - pythonSdk: [{ type: 'autogenerated', dirName: 'python-sdk' }], - typescriptSdk: [{ type: 'autogenerated', dirName: 'typescript-sdk' }], - mcpServer: [{ type: 'autogenerated', dirName: 'mcp-server' }], - tutorials: [{ type: 'autogenerated', dirName: 'tutorials' }], docs: [ { type: 'link', - label: 'Python SDK', - href: '/python-sdk/introduction', - className: 'python-sdk sdk-link' + label: 'Quick Start', + href: '/get-started/quick-tutorial', + customProps: { icon: 'Rocket' } }, { - type: 'link', - label: 'TypeScript SDK', - href: '/typescript-sdk/introduction', - className: 'typescript-sdk sdk-link' - }, - { - type: 'link', - label: 'REST API', - href: '/rest-api/introduction', - className: 'rest-api sdk-link' - }, - { - type: 'link', - label: 'MCP Server', - href: '/mcp-server/introduction', - className: 'mcp-server sdk-link' - }, - { - type: 'link', - label: 'Tutorials', - href: '/tutorials', - className: 'tutorials-link sdk-link' + type: 'category', + label: 'Deploy', + collapsible: false, + collapsed: false, + customProps: { icon: 'Server' }, + items: [ + { + type: 'category', + label: 'Local Hosting', + collapsible: true, + collapsed: true, + customProps: { icon: 'Monitor' }, + link: { type: 'doc', id: 'deploy/local-hosting/index' }, + items: ['deploy/local-hosting/docker', 'deploy/local-hosting/from-source', 'deploy/local-hosting/helm'] + }, + { + type: 'category', + label: 'Remote Hosting', + collapsible: true, + collapsed: true, + customProps: { icon: 'Cloud' }, + link: { type: 'doc', id: 'deploy/remote-hosting/index' }, + items: [ + 'deploy/remote-hosting/prerequisites', + 'deploy/remote-hosting/digital-ocean', + 'deploy/remote-hosting/aws-gcp-azure', + 'deploy/remote-hosting/self-hosting-rushdb' + ] + }, + { + type: 'category', + label: 'Configuration', + collapsible: true, + collapsed: true, + customProps: { icon: 'Settings2' }, + link: { type: 'doc', id: 'deploy/configuration/index' }, + items: ['deploy/configuration/environment-variables', 'deploy/configuration/get-api-key', 'deploy/configuration/security'] + }, + { + type: 'category', + label: 'Infrastructure', + collapsible: true, + collapsed: true, + customProps: { icon: 'Database' }, + link: { type: 'doc', id: 'deploy/infrastructure/index' }, + items: ['deploy/infrastructure/rushdb-platform', 'deploy/infrastructure/neo4j-and-aura', 'deploy/infrastructure/postgresql-sqlite'] + } + ] }, { type: 'category', - label: 'Getting Started', + label: 'Connect', + collapsible: false, collapsed: false, - items: ['index', 'get-started/quick-tutorial', 'get-started/get-api-key'] + customProps: { icon: 'Plug' }, + link: { type: 'doc', id: 'connect/index' }, + items: [ + { + type: 'link', + label: 'Skills', + href: '/connect/skills', + className: 'connect-sidebar-link connect-sidebar-link--skills', + customProps: { icon: 'Sparkles' } + }, + { + type: 'link', + label: 'MCP', + href: '/connect/mcp', + className: 'connect-sidebar-link connect-sidebar-link--mcp', + customProps: { icon: 'Cpu' } + }, + { + type: 'link', + label: 'Agents', + href: '/connect/agents', + className: 'connect-sidebar-link connect-sidebar-link--agents', + customProps: { icon: 'Bot' } + }, + { + type: 'link', + label: 'SDK & REST', + href: '/connect/sdks', + className: 'connect-sidebar-link connect-sidebar-link--sdks', + customProps: { icon: 'Code2' } + } + ] }, - 'concepts/agent-memory-model', - 'concepts/ontology-schema-discovery', { type: 'category', - label: 'Core Concepts', + label: 'Learn', + collapsible: false, collapsed: false, + customProps: { icon: 'Hammer' }, items: [ - 'concepts/index', - 'concepts/records', - 'concepts/data-ingestion', - 'concepts/labels', - 'concepts/properties', - 'concepts/relationships', - 'concepts/semantic-search', - 'concepts/bring-your-own-vectors', - 'concepts/storage', - 'concepts/transactions', { - label: 'Search', + type: 'category', + label: 'Agent Memory', + collapsible: true, + collapsed: false, + customProps: { icon: 'Brain' }, + link: { type: 'doc', id: 'learn/agent-memory/index' }, + items: ['learn/agent-memory/quickstart', 'learn/agent-memory/schema-self-awareness'] + }, + { + type: 'category', + label: 'Records & Queries', + collapsible: true, + collapsed: true, + customProps: { icon: 'Database' }, + link: { type: 'doc', id: 'learn/records-and-queries/index' }, + items: [ + 'learn/records-and-queries/store-records', + 'learn/records-and-queries/import-data', + 'learn/records-and-queries/find-and-query', + 'learn/records-and-queries/raw-queries', + 'learn/records-and-queries/export-data', + 'learn/records-and-queries/labels-and-properties', + 'learn/records-and-queries/discover-your-schema', + 'learn/records-and-queries/labeled-meta-property-graph', + 'learn/records-and-queries/transactions' + ] + }, + { + type: 'category', + label: 'SearchQuery', + collapsible: true, + collapsed: true, + customProps: { icon: 'Filter' }, + link: { type: 'doc', id: 'learn/search-query/search-query' }, + items: [ + 'learn/search-query/where-operators', + 'learn/search-query/select-expressions', + 'learn/search-query/group-by', + 'learn/search-query/pagination-order', + 'learn/search-query/search-labels' + ] + }, + { + type: 'category', + label: 'Relationships', + collapsible: true, + collapsed: true, + customProps: { icon: 'Network' }, + link: { type: 'doc', id: 'learn/relationships/index' }, + items: [ + 'learn/relationships/connect-records', + 'learn/relationships/bulk-relationships', + 'learn/relationships/suggested-patterns' + ] + }, + { + type: 'category', + label: 'Semantic Search', + collapsible: true, + collapsed: true, + customProps: { icon: 'Search' }, + link: { type: 'doc', id: 'learn/semantic-search/semantic-search' }, + items: [ + 'learn/semantic-search/manage-indexes', + 'learn/semantic-search/write-with-vectors', + 'learn/semantic-search/bring-your-own-vectors' + ] + }, + { + type: 'category', + label: 'Tutorials', collapsible: true, collapsed: true, + customProps: { icon: 'BookOpen' }, + link: { type: 'doc', id: 'learn/tutorials/index' }, + items: [ + { + type: 'category', + label: 'AI & RAG', + collapsible: true, + collapsed: true, + customProps: { icon: 'Cpu' }, + items: [ + 'learn/tutorials/ai-and-rag/ai-semantic-search', + 'learn/tutorials/ai-and-rag/graphrag', + 'learn/tutorials/ai-and-rag/hybrid-retrieval', + 'learn/tutorials/ai-and-rag/rag-pipeline', + 'learn/tutorials/ai-and-rag/rag-evaluation', + 'learn/tutorials/ai-and-rag/rag-multi-source', + 'learn/tutorials/ai-and-rag/rag-reranking', + 'learn/tutorials/ai-and-rag/semantic-search-multitenant', + 'learn/tutorials/ai-and-rag/explainable-results', + 'learn/tutorials/ai-and-rag/byov-when-and-why', + 'learn/tutorials/ai-and-rag/byov-external-embeddings' + ] + }, + { + type: 'category', + label: 'Agent Memory', + collapsible: true, + collapsed: true, + customProps: { icon: 'Brain' }, + items: [ + 'learn/tutorials/agent-memory/agent-safe-query-planning', + 'learn/tutorials/agent-memory/agent-skills-with-openclaw', + 'learn/tutorials/agent-memory/building-team-memory', + 'learn/tutorials/agent-memory/episodic-memory', + 'learn/tutorials/agent-memory/memory-layer', + 'learn/tutorials/agent-memory/research-knowledge-graph' + ] + }, + { + type: 'category', + label: 'Graph Modeling', + collapsible: true, + collapsed: true, + customProps: { icon: 'Network' }, + items: [ + 'learn/tutorials/graph-modeling/thinking-in-graphs', + 'learn/tutorials/graph-modeling/choosing-relationship-types', + 'learn/tutorials/graph-modeling/modeling-hierarchies', + 'learn/tutorials/graph-modeling/temporal-graphs', + 'learn/tutorials/graph-modeling/data-lineage', + 'learn/tutorials/graph-modeling/versioning-records', + 'learn/tutorials/graph-modeling/customer-360' + ] + }, + { + type: 'category', + label: 'Working with Data', + collapsible: true, + collapsed: true, + customProps: { icon: 'Database' }, + items: [ + 'learn/tutorials/working-with-data/importing-data', + 'learn/tutorials/working-with-data/importing-from-mongodb', + 'learn/tutorials/working-with-data/event-driven-ingestion', + 'learn/tutorials/working-with-data/third-party-webhook-ingestion', + 'learn/tutorials/working-with-data/graph-backed-api' + ] + }, + { + type: 'category', + label: 'Search & Queries', + collapsible: true, + collapsed: true, + customProps: { icon: 'Search' }, + items: [ + 'learn/tutorials/search-and-queries/discovery-queries', + 'learn/tutorials/search-and-queries/reusable-search-query', + 'learn/tutorials/search-and-queries/searchquery-advanced-patterns', + 'learn/tutorials/search-and-queries/search-ux-patterns', + 'learn/tutorials/search-and-queries/query-optimization', + 'learn/tutorials/search-and-queries/testing-searchquery' + ] + }, + { + type: 'category', + label: 'Use Cases', + collapsible: true, + collapsed: true, + customProps: { icon: 'Globe' }, + items: [ + 'learn/tutorials/use-cases/audit-trails', + 'learn/tutorials/use-cases/compliance-retention', + 'learn/tutorials/use-cases/incident-response', + 'learn/tutorials/use-cases/supply-chain-traceability', + 'learn/tutorials/use-cases/byoc-vs-managed', + 'learn/tutorials/use-cases/is-rushdb-right-for-me' + ] + } + ] + }, + { type: 'category', + label: 'Reference', + collapsible: true, + collapsed: true, + customProps: { icon: 'BookMarked' }, items: [ { - type: 'autogenerated', - dirName: 'concepts/search' + type: 'category', + label: 'TypeScript', + collapsible: true, + collapsed: true, + customProps: { icon: 'FileCode' }, + link: { type: 'doc', id: 'learn/reference/typescript/index' }, + items: [ + 'learn/reference/typescript/DBRecord', + 'learn/reference/typescript/DBRecordInstance', + 'learn/reference/typescript/DBRecordTarget', + 'learn/reference/typescript/DBRecordsArrayInstance', + 'learn/reference/typescript/Model', + 'learn/reference/typescript/RelationTarget', + 'learn/reference/typescript/RushDB', + 'learn/reference/typescript/SearchQuery', + 'learn/reference/typescript/Transaction' + ] + }, + { + type: 'category', + label: 'Python', + collapsible: true, + collapsed: true, + customProps: { icon: 'FileCode' }, + link: { type: 'doc', id: 'learn/reference/python/index' }, + items: [ + 'learn/reference/python/record', + 'learn/reference/python/RushDB', + 'learn/reference/python/search-result', + 'learn/reference/python/SearchQuery', + 'learn/reference/python/transaction' + ] + }, + { + type: 'category', + label: 'REST API', + collapsible: true, + collapsed: true, + customProps: { icon: 'Globe' }, + link: { type: 'doc', id: 'learn/reference/rest-api/introduction' }, + items: [ + { + type: 'category', + label: 'Records', + collapsible: true, + collapsed: true, + customProps: { icon: 'Database' }, + items: [ + 'learn/reference/rest-api/records/create-records', + 'learn/reference/rest-api/records/get-records', + 'learn/reference/rest-api/records/update-records', + 'learn/reference/rest-api/records/delete-records', + 'learn/reference/rest-api/records/import-data', + 'learn/reference/rest-api/records/export-data' + ] + }, + { + type: 'category', + label: 'AI & Vectors', + collapsible: true, + collapsed: true, + customProps: { icon: 'Sparkles' }, + link: { type: 'doc', id: 'learn/reference/rest-api/ai-and-vectors/overview' }, + items: [ + 'learn/reference/rest-api/ai-and-vectors/indexing', + 'learn/reference/rest-api/ai-and-vectors/advanced-indexing', + 'learn/reference/rest-api/ai-and-vectors/search', + 'learn/reference/rest-api/ai-and-vectors/write-with-vectors' + ] + }, + 'learn/reference/rest-api/labels', + 'learn/reference/rest-api/properties', + 'learn/reference/rest-api/relationships', + 'learn/reference/rest-api/transactions', + 'learn/reference/rest-api/raw-queries' + ] + }, + { + type: 'category', + label: 'MCP', + collapsible: true, + collapsed: true, + customProps: { icon: 'Cpu' }, + link: { type: 'doc', id: 'learn/reference/mcp/index' }, + items: ['learn/reference/mcp/tools'] } ] } @@ -76,9 +377,17 @@ const sidebars: SidebarsConfig = { }, { type: 'category', - label: 'Pricing & Billing', + label: 'RushDB Cloud', + collapsible: true, collapsed: false, - items: ['concepts/knowledge-units', 'concepts/billing-model'] + customProps: { icon: 'Cloud' }, + link: { type: 'doc', id: 'rushdb-cloud/index' }, + items: [ + 'rushdb-cloud/billing-model', + 'rushdb-cloud/project-isolation', + 'rushdb-cloud/licensing-and-services', + 'rushdb-cloud/knowledge-units' + ] } ] } diff --git a/docs/src/components/CopyPageButton.tsx b/docs/src/components/CopyPageButton.tsx index dbd87944..73ce19e4 100644 --- a/docs/src/components/CopyPageButton.tsx +++ b/docs/src/components/CopyPageButton.tsx @@ -1,11 +1,22 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import BrowserOnly from '@docusaurus/BrowserOnly' import { useLocation } from '@docusaurus/router' -import { Clipboard, Check, ChevronDown, FileText, Maximize2, Minimize2 } from 'lucide-react' +import { Check, ChevronDown, Maximize2, Minimize2, FileTextIcon } from 'lucide-react' const GITHUB_RAW = 'https://raw.githubusercontent.com/rush-db/rushdb/main/docs/docs' const DOCS_BASE = 'https://docs.rushdb.com' +async function fetchMarkdownSource(url: string): Promise { + const response = await fetch(url) + if (!response.ok) return null + + const text = await response.text() + const contentType = response.headers.get('content-type')?.toLowerCase() ?? '' + const isHtml = contentType.includes('text/html') || /^\s*]/i.test(text) + + return isHtml ? null : text +} + function ClaudeIcon() { return ( { setResolvedMarkdownUrl(null) - fetch(markdownUrl, { method: 'HEAD' }) - .then((r) => { - if (r.ok) { - setResolvedMarkdownUrl(markdownUrl) - return + let cancelled = false + + const resolveMarkdownUrl = async () => { + for (const candidate of markdownCandidates) { + try { + const source = await fetchMarkdownSource(candidate) + if (source != null) { + if (!cancelled) setResolvedMarkdownUrl(candidate) + return + } + } catch { + // Try the next local or GitHub source candidate. } - fetch(markdownUrlMdx, { method: 'HEAD' }) - .then((r2) => { - setResolvedMarkdownUrl(r2.ok ? markdownUrlMdx : markdownUrl) - }) - .catch(() => setResolvedMarkdownUrl(markdownUrl)) - }) - .catch(() => setResolvedMarkdownUrl(markdownUrl)) + } + + if (!cancelled) setResolvedMarkdownUrl(localMarkdownUrlMdx) + } + + void resolveMarkdownUrl() + return () => { + cancelled = true + } }, [slug]) - const effectiveMarkdownUrl = resolvedMarkdownUrl ?? markdownUrl + const effectiveMarkdownUrl = resolvedMarkdownUrl ?? localMarkdownUrlMdx const handleCopy = useCallback(async () => { + let content: string | null = null + try { - const res = await fetch(effectiveMarkdownUrl) - const text = res.ok ? await res.text() : null - const content = text ?? `# ${document.title}\n\n${window.location.href}` + for (const candidate of [effectiveMarkdownUrl, ...markdownCandidates]) { + try { + const source = await fetchMarkdownSource(candidate) + if (source != null) { + content = source + break + } + } catch { + // Try the next local or GitHub source candidate. + } + } + + content ??= `# ${document.title}\n\n${window.location.href}` await navigator.clipboard.writeText(content) } catch { - // fallback: copy the URL when fetch/clipboard fails + content ??= `# ${document.title}\n\n${window.location.href}` try { - await navigator.clipboard.writeText(window.location.href) + await navigator.clipboard.writeText(content) } catch { const el = document.createElement('textarea') - el.value = window.location.href + el.value = content document.body.appendChild(el) el.select() document.execCommand('copy') @@ -172,7 +220,7 @@ function CopyPageButtonInner() { } setCopied(true) setTimeout(() => setCopied(false), 2000) - }, [effectiveMarkdownUrl]) + }, [effectiveMarkdownUrl, slug]) useEffect(() => { if (!open) return @@ -195,11 +243,11 @@ function CopyPageButtonInner() { return (
{/* Main copy button */} - {/* Dropdown chevron */} diff --git a/docs/src/components/DeployMethodCard.tsx b/docs/src/components/DeployMethodCard.tsx new file mode 100644 index 00000000..d04ac9c9 --- /dev/null +++ b/docs/src/components/DeployMethodCard.tsx @@ -0,0 +1,365 @@ +import React from 'react' +import Link from '@docusaurus/Link' +import { + ArrowRight, + Box, + Check, + ClipboardList, + Clock, + Cloud, + Code2, + Globe, + Layers, + Server, + Terminal, + type LucideIcon +} from 'lucide-react' +import { cn } from './ui/lib/utils' + +const ICON_MAP: Record = { + Box, + ClipboardList, + Cloud, + Code2, + Globe, + Layers, + Server +} + +const BORDER_CLASS = 'border-[var(--ifm-color-emphasis-200)]' + +// ── Badge config ─────────────────────────────────────────────────────────────── + +type BadgeVariant = 'beginner' | 'intermediate' | 'advanced' | 'cloud' | 'self-hosted' | 'byoc' | 'required' + +const BADGE_STYLES: Record = { + beginner: { label: 'Beginner', bg: '#16a34a', color: '#fff' }, + intermediate: { label: 'Intermediate', bg: '#d97706', color: '#fff' }, + advanced: { label: 'Advanced', bg: '#dc2626', color: '#fff' }, + cloud: { label: 'Cloud', bg: '#3f81ff', color: '#fff' }, + 'self-hosted': { label: 'Self-Hosted', bg: '#7c3aed', color: '#fff' }, + byoc: { label: 'BYOC', bg: '#0891b2', color: '#fff' }, + required: { label: 'Required', bg: '#475569', color: '#fff' } +} + +// ── Types ────────────────────────────────────────────────────────────────────── + +export type DeployMethodCardProps = { + title: string + description: string + badge: BadgeVariant + time?: string + prerequisites?: string[] + commands?: string[] + tags?: string[] + href: string + recommended?: boolean + icon?: string +} + +// ── Card ─────────────────────────────────────────────────────────────────────── + +export function DeployMethodCard({ + title, + description, + badge, + time, + prerequisites = [], + commands = [], + tags = [], + href +}: DeployMethodCardProps) { + const b = BADGE_STYLES[badge] + + return ( + + {/* Header row */} +
+

{title}

+
+ {time && ( + + + {time} + + )} + + {b.label} + +
+
+ + {/* Description */} +

{description}

+ + {/* Prerequisites + Commands */} + {(prerequisites.length > 0 || commands.length > 0) && ( +
0 && prerequisites.length > 0 ? 'grid-cols-2' : 'grid-cols-1' + )} + > + {prerequisites.length > 0 && ( +
+

+ Prerequisites +

+
    + {prerequisites.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+ )} + {commands.length > 0 && ( +
+

+ Key Commands +

+
    + {commands.map((cmd) => ( +
  • + + {cmd} +
  • + ))} +
+
+ )} +
+ )} + + {/* Tags */} + {tags.length > 0 && ( +
+ {tags.map((t) => ( + + {t} + + ))} +
+ )} + + {/* CTA */} + + View full guide + + + ) +} + +// ── Grid wrapper ─────────────────────────────────────────────────────────────── + +export function DeployMethodCardGrid({ children }: { children: React.ReactNode }) { + return
{children}
+} + +// ── Cross-link section ───────────────────────────────────────────────────────── + +type CrossLink = { label: string; href: string } + +export function DeployCrossLinks({ prefix, links }: { prefix?: string; links: CrossLink[] }) { + return ( +
+ {prefix &&

{prefix}

} +
+ {links.map((l) => ( + + {l.label} + + ))} +
+
+ ) +} + +// ── Row (full-width) ─────────────────────────────────────────────────────────── + +export type DeployMethodRowProps = DeployMethodCardProps + +export function DeployMethodRow({ + title, + description, + badge, + time, + prerequisites = [], + commands = [], + tags = [], + href, + recommended, + icon +}: DeployMethodRowProps) { + const b = BADGE_STYLES[badge] + const Icon = icon ? ICON_MAP[icon] : null + + return ( + + {/* Header: icon + title/badges/description + time */} +
+
+ {Icon && ( +
+ +
+ )} +
+
+

+ {title} +

+ {recommended && ( + + Recommended + + )} + + {b.label} + +
+

{description}

+
+
+ {time && ( + + + {time} + + )} +
+ + {/* Prerequisites + Commands */} + {(prerequisites.length > 0 || commands.length > 0) && ( +
0 && prerequisites.length > 0 ? 'grid-cols-2' : 'grid-cols-1' + )} + > + {prerequisites.length > 0 && ( +
+

+ Prerequisites +

+
    + {prerequisites.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+ )} + {commands.length > 0 && ( +
+

+ Key Commands +

+
    + {commands.map((cmd) => ( +
  • + + {cmd} +
  • + ))} +
+
+ )} +
+ )} + + {/* Footer: tags + CTA */} +
+ {tags.length > 0 ? +
+ {tags.map((t) => ( + + {t} + + ))} +
+ :
} + + View full guide + +
+ + ) +} + +// ── Row list wrapper ─────────────────────────────────────────────────────────── + +export function DeployMethodRowList({ children }: { children: React.ReactNode }) { + return
{children}
+} diff --git a/docs/src/components/DocsHomePage.tsx b/docs/src/components/DocsHomePage.tsx deleted file mode 100644 index 871fc591..00000000 --- a/docs/src/components/DocsHomePage.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import React from 'react' -import { - ArrowRight, - Book, - Check, - Globe, - Monitor, - Rocket, - Share2, - Shield, - Sparkles, - Sun, - Upload -} from 'lucide-react' - -const BORDER_CLASS = 'border-[var(--ifm-color-emphasis-200)]' - -// ── Icons ───────────────────────────────────────────────────────────────────── - -const TypeScriptIcon = () => ( - -) - -const PythonIcon = () => ( - -) - -// ── Data ────────────────────────────────────────────────────────────────────── - -type InterfaceCard = { - icon: () => React.ReactElement - label: string - badge: string - badgeColor: string - description: string - features: string[] - href: string -} - -const INTERFACE_CARDS: InterfaceCard[] = [ - { - icon: TypeScriptIcon, - label: 'TypeScript SDK', - badge: 'npm', - badgeColor: '#3178C6', - description: 'Full type safety for Node.js and browsers. Async/await API with zero config.', - features: ['Type-safe query builder', 'Browser + Node.js', 'ESM & CommonJS'], - href: '/typescript-sdk/introduction' - }, - { - icon: PythonIcon, - label: 'Python SDK', - badge: 'pip', - badgeColor: '#3776AB', - description: 'Ergonomic client for backend scripts and data workflows. Sync and async.', - features: ['Sync & async client', 'Pandas-friendly output', 'Pipeline-ready'], - href: '/python-sdk/introduction' - }, - { - icon: () => , - label: 'REST API', - badge: 'HTTP', - badgeColor: '#16a34a', - description: 'Language-agnostic HTTP access. Works from any stack with curl or fetch.', - features: ['OpenAPI spec', 'Full CRUD + search', 'Transaction support'], - href: '/rest-api/introduction' - }, - { - icon: () => , - label: 'MCP Server', - badge: 'MCP', - badgeColor: '#7c3aed', - description: 'Model Context Protocol server. Give any LLM agent direct database access.', - features: ['Claude, GPT & Cursor', 'Schema auto-exposed', 'One-command deploy'], - href: '/mcp-server/introduction' - } -] - -type ResourceCard = { - icon: () => React.ReactElement - label: string - description: string - cta: string - href: string -} - -const RESOURCE_CARDS: ResourceCard[] = [ - { - icon: () => , - label: 'Quick Tutorial', - description: 'First write → graph → semantic search in under 10 minutes.', - cta: 'Start here', - href: '/get-started/quick-tutorial' - }, - { - icon: () => , - label: 'Tutorials', - description: 'Hands-on guides: RAG pipelines, deployment, vector search, agent memory.', - cta: 'Browse tutorials', - href: '/tutorials' - }, - { - icon: () => , - label: 'Concepts', - description: 'How RushDB stores, links, and queries data — records, properties, graphs.', - cta: 'Learn concepts', - href: '/concepts' - } -] - -type FeatureItem = { - icon: () => React.ReactElement - title: string - description: string - accent: string - href: string -} - -const FEATURE_ITEMS: FeatureItem[] = [ - { - icon: () => , - title: 'Ingest anything', - description: - 'Push flat objects, nested trees, or batches. Types inferred, graph built — no schema needed.', - accent: '#3f81ff', - href: '/concepts/data-ingestion' - }, - { - icon: () => , - title: 'Auto-linked graph', - description: 'Nested objects become linked records automatically. Traverse relationships in queries.', - accent: '#8b5cf6', - href: '/concepts/relationships' - }, - { - icon: () => , - title: 'Semantic search', - description: 'Index any text property and query by meaning. Combine with field filters.', - accent: '#f59e0b', - href: '/concepts/semantic-search' - }, - { - icon: () => , - title: 'ACID transactions', - description: 'Wrap any combination of writes and reads. Nothing persists if any step fails.', - accent: '#10b981', - href: '/concepts/transactions' - } -] - -// ── Sub-components ──────────────────────────────────────────────────────────── - -function InterfaceCard({ card: c }: { card: InterfaceCard }) { - return ( - - {/* Header */} -
-
- -
- - {c.badge} - -
- - {/* Title & description */} -

{c.label}

-

- {c.description} -

- - {/* Features */} -
    - {c.features.map((f) => ( -
  • - - - - {f} -
  • - ))} -
- - {/* CTA */} - - Get started - -
- ) -} - -function ResourceCard({ card: c }: { card: ResourceCard }) { - return ( - -
- -
-

{c.label}

-

- {c.description} -

- - {c.cta} - -
- ) -} - -function FeaturePill({ item }: { item: FeatureItem }) { - return ( - - - - -
-

{item.title}

-

- {item.description} -

-
-
- ) -} - -// ── Main component ──────────────────────────────────────────────────────────── - -export default function DocsHomePage() { - return ( -
- {/* ── Hero ─────────────────────────────────────────────────────────── */} -
- - [ instant graph + vector storage ] - - -

- - Memory for agents - - - and apps. Instant. - -

- -

- Push any JSON or CSV. Get graph relationships and vector search automatically. -
- No schema. No pipeline. No glue code. -

- - -
- - {/* ── Features grid ────────────────────────────────────────────────── */} -
- {FEATURE_ITEMS.map((item) => ( - - ))} -
- - {/* ── Interfaces ───────────────────────────────────────────────────── */} -
-
-

- Choose your interface -

-

- Every interface gives you the same capabilities — pick the one that fits your stack. -

-
-
- {INTERFACE_CARDS.map((card) => ( - - ))} -
-
- - {/* ── Divider ──────────────────────────────────────────────────────── */} -
- - {/* ── Learning resources ───────────────────────────────────────────── */} -
-
-

Start learning

-

- Guides, tutorials, and concept explanations to get you productive fast. -

-
-
- {RESOURCE_CARDS.map((card) => ( - - ))} -
-
- - {/* ── Deployment CTA ───────────────────────────────────────────────── */} -
-
-

- Cloud -

-

RushDB Cloud

-

- Free tier — 100,000 KU/month and 2 projects. No credit card required. -

- - Sign up free - -
- -
-

- Self-Hosted -

-

- Run on your infrastructure -

-

- No KU limits, no billing. Deploy with Docker in minutes on your own stack. -

- - Deployment guide - -
-
-
- ) -} diff --git a/docs/src/components/SidebarCommunity.tsx b/docs/src/components/SidebarCommunity.tsx new file mode 100644 index 00000000..e7e8cc1d --- /dev/null +++ b/docs/src/components/SidebarCommunity.tsx @@ -0,0 +1,43 @@ +import React from 'react' + +export default function SidebarCommunity({ className }: { className?: string }) { + return ( +
+ + Blog + + + X (Twitter) + + + LinkedIn + + + GitHub + + + © {new Date().getFullYear()}, Collect Software Inc. + +
+ ) +} diff --git a/docs/src/components/ThemeSwitch.tsx b/docs/src/components/ThemeSwitch.tsx new file mode 100644 index 00000000..19778116 --- /dev/null +++ b/docs/src/components/ThemeSwitch.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { Moon, Sun } from 'lucide-react' + +type Props = { + className?: string + disabled?: boolean + isDark: boolean + onToggle: () => void +} + +export default function ThemeSwitch({ className, disabled, isDark, onToggle }: Props) { + return ( + + ) +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 3abb5e11..914d34f2 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -7,441 +7,952 @@ /* Import fonts */ - :root { - - /* LP light accent — orange */ - --ifm-color-primary: #C8540A; - --ifm-color-primary-dark: #b04808; - --ifm-color-primary-darker: #9a3f07; - --ifm-color-primary-darkest: #7a3205; - --ifm-color-primary-light: #d96b22; - --ifm-color-primary-lighter: #e0813d; - --ifm-color-primary-lightest: #eda87a; - - /* Font settings */ - --ifm-font-family-base: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; - --ifm-font-family-monospace: 'JetBrains Mono', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - --ifm-code-font-size: 14px; - --docusaurus-highlighted-code-line-bg: rgba(200, 84, 10, 0.06); - - --ifm-navbar-height: 4rem; - - --ifm-container-width-xl: 1280px; - --ifm-container-width: 1280px; - - /* LP warm beige palette */ - --ifm-navbar-background-color: rgba(237, 233, 224, 0.88); - --ifm-footer-background-color: #EDE9E0; - --ifm-background-color: #EDE9E0; - --ifm-toc-border-color: #DAD6CE; - --ifm-code-background: #F5F2EB; - --ifm-card-background-color: #F5F2EB; - --ifm-background-surface-color: #F5F2EB; - - --ifm-color-emphasis-100: #F5F2EB; - --ifm-color-emphasis-200: #DAD6CE; - --ifm-color-emphasis-300: #c8c3b9; - --ifm-color-emphasis-400: #b0aa9e; - --ifm-color-emphasis-500: #7A7268; - --ifm-color-emphasis-600: #5c5650; - --ifm-color-emphasis-700: #3d3830; - - --ifm-hr-background-color: var(--ifm-toc-border-color); - - --doc-sidebar-width: 260px !important; - - --ifm-list-left-padding: 1rem; - - --ifm-font-color-base: #1C1A16; - --ifm-footer-link-color: #1C1A16; - --ifm-menu-color: #7A7268; - - --ifm-font-weight-semibold: 600; + /* LP light accent — orange */ + --ifm-color-primary: #c8540a; + --ifm-color-primary-dark: #b04808; + --ifm-color-primary-darker: #9a3f07; + --ifm-color-primary-darkest: #7a3205; + --ifm-color-primary-light: #d96b22; + --ifm-color-primary-lighter: #e0813d; + --ifm-color-primary-lightest: #eda87a; + + /* Font settings */ + --ifm-font-family-base: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + --ifm-font-family-monospace: 'JetBrains Mono', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace; + --ifm-code-font-size: 14px; + --docusaurus-highlighted-code-line-bg: rgba(200, 84, 10, 0.06); + + /* Header hidden for now; keep navbar offset at zero */ + --ifm-navbar-height: 0px; + + --ifm-container-width-xl: 1280px; + --ifm-container-width: 1280px; + + /* LP warm beige palette */ + --ifm-navbar-background-color: rgba(237, 233, 224, 0.88); + --ifm-footer-background-color: #ede9e0; + --ifm-background-color: #ede9e0; + --ifm-toc-border-color: #dad6ce; + --ifm-code-background: #f5f2eb; + --ifm-card-background-color: #f5f2eb; + --ifm-background-surface-color: #f5f2eb; + + --ifm-color-emphasis-100: #f5f2eb; + --ifm-color-emphasis-200: #dad6ce; + --ifm-color-emphasis-300: #c8c3b9; + --ifm-color-emphasis-400: #b0aa9e; + --ifm-color-emphasis-500: #7a7268; + --ifm-color-emphasis-600: #5c5650; + --ifm-color-emphasis-700: #3d3830; + + --ifm-hr-background-color: var(--ifm-toc-border-color); + + --doc-sidebar-width: 300px !important; + + --ifm-list-left-padding: 1rem; + + --ifm-font-color-base: #1c1a16; + --ifm-footer-link-color: #1c1a16; + --ifm-menu-color: #7a7268; + + --ifm-font-weight-semibold: 600; } body { - font-family: var(--ifm-font-family-base); - -webkit-font-smoothing: initial; /* For Chrome, Safari */ - -moz-osx-font-smoothing: grayscale; /* For Firefox on macOS */ - font-smoothing: antialiased; /* General property, less widely supported */ + font-family: var(--ifm-font-family-base); + -webkit-font-smoothing: initial; /* For Chrome, Safari */ + -moz-osx-font-smoothing: grayscale; /* For Firefox on macOS */ + font-smoothing: antialiased; /* General property, less widely supported */ } /* For readability concerns, you should choose a lighter palette in dark mode. */ html[data-theme='dark'] { - font-weight: 400; + font-weight: 400; - --ifm-font-color-base: #F0F0EE; + --ifm-font-color-base: #f0f0ee; - /* --ifm-toc-link-color: var(--ifm-toc-link-color); */ - --ifm-menu-color: #7b7b84; - --ifm-menu-color-active: var(--ifm-color-primary); + /* --ifm-toc-link-color: var(--ifm-toc-link-color); */ + --ifm-menu-color: #7b7b84; + --ifm-menu-color-active: var(--ifm-color-primary); - --ifm-background-color: #0A0A0B; - --ifm-navbar-background-color: rgba(10, 10, 11, 0.88); - --ifm-footer-background-color: #0A0A0B; - --ifm-code-background: #111113; - --docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.07); + --ifm-background-color: #0a0a0b; + --ifm-navbar-background-color: rgba(10, 10, 11, 0.88); + --ifm-footer-background-color: #0a0a0b; + --ifm-code-background: #111113; + --docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.07); - --ifm-toc-border-color: #1E1E22; + --ifm-toc-border-color: #1e1e22; - --ifm-hr-background-color: var(--ifm-toc-border-color); + --ifm-hr-background-color: var(--ifm-toc-border-color); - --ifm-footer-link-color: var(--ifm-menu-color); + --ifm-footer-link-color: var(--ifm-menu-color); - /* LP green accent */ - --ifm-color-primary: #00FF85; - --ifm-color-primary-dark: #00e876; - --ifm-color-primary-darker: #00cc69; - --ifm-color-primary-darkest: #00a855; - --ifm-color-primary-light: #33ff9d; - --ifm-color-primary-lighter: #66ffb5; - --ifm-color-primary-lightest: #99ffcc; + /* LP green accent */ + --ifm-color-primary: #00ff85; + --ifm-color-primary-dark: #00e876; + --ifm-color-primary-darker: #00cc69; + --ifm-color-primary-darkest: #00a855; + --ifm-color-primary-light: #33ff9d; + --ifm-color-primary-lighter: #66ffb5; + --ifm-color-primary-lightest: #99ffcc; - /* Card and surface colors */ - --ifm-card-background-color: #111113; - --ifm-color-emphasis-100: #17171a; - --ifm-color-emphasis-200: #1E1E22; - --ifm-color-emphasis-300: #28282d; - --ifm-color-emphasis-400: #36363d; - --ifm-color-emphasis-500: #7b7b84; - --ifm-color-emphasis-600: #9f9faa; - --ifm-color-emphasis-700: #c0c0cc; - --ifm-background-surface-color: #111113; + /* Card and surface colors */ + --ifm-card-background-color: #111113; + --ifm-color-emphasis-100: #17171a; + --ifm-color-emphasis-200: #1e1e22; + --ifm-color-emphasis-300: #28282d; + --ifm-color-emphasis-400: #36363d; + --ifm-color-emphasis-500: #7b7b84; + --ifm-color-emphasis-600: #9f9faa; + --ifm-color-emphasis-700: #c0c0cc; + --ifm-background-surface-color: #111113; } - .footer { - border-top: 1px solid var(--ifm-toc-border-color); - padding: 4rem 0; - background-color: var(--ifm-background-color) !important; - color: var(--ifm-footer-color); + border-top: 1px solid var(--ifm-toc-border-color); + padding: 4rem 0; + background-color: var(--ifm-background-color) !important; + color: var(--ifm-footer-color); } .footer__title { - /*color: var(--ifm-footer-title-color);*/ - font-weight: 600; + /*color: var(--ifm-footer-title-color);*/ + font-weight: 600; } .footer__link-item { - color: var(--ifm-footer-link-color); - font-weight: 500; + color: var(--ifm-footer-link-color); + font-weight: 500; } .footer__link-item:hover { - color: var(--ifm-color-primary); - text-decoration: none; + color: var(--ifm-color-primary); + text-decoration: none; } .navbar { - box-shadow: none; - border-bottom: 1px solid var(--ifm-toc-border-color); - backdrop-filter: blur(12px); - background-color: var(--ifm-navbar-background-color); - font-weight: 600; + display: none !important; + box-shadow: none; + border-bottom: 1px solid var(--ifm-toc-border-color); + backdrop-filter: blur(12px); + background-color: var(--ifm-navbar-background-color); + font-weight: 600; } /* Inactive left-side SDK/API nav links — same color as inactive sidebar items */ .navbar__link { - --ifm-navbar-link-color: var(--ifm-menu-color); + --ifm-navbar-link-color: var(--ifm-menu-color); } .navbar__link::before { - background-color: var(--ifm-menu-color) !important; + background-color: var(--ifm-menu-color) !important; } /* Dark mode: bump inactive up slightly so it's readable on the dark navbar */ -html[data-theme='dark'] .navbar__link { - /* --ifm-navbar-link-color: rgba(255, 255, 255, 0.5); */ - --ifm-navbar-link-color: var(--ifm-menu-color) !important; +html[data-theme='dark'] .navbar__link { + /* --ifm-navbar-link-color: rgba(255, 255, 255, 0.5); */ + --ifm-navbar-link-color: var(--ifm-menu-color) !important; } -html[data-theme='dark'] .navbar__link::before { - background-color: rgba(255, 255, 255, 0.5) !important; +html[data-theme='dark'] .navbar__link::before { + background-color: rgba(255, 255, 255, 0.5) !important; } /* Active link */ .navbar__link--active { - --ifm-navbar-link-color: var(--ifm-color-primary); - text-shadow: rgb(200 84 10 / 18%) 0px 0px 16px - + --ifm-navbar-link-color: var(--ifm-color-primary); + text-shadow: rgb(200 84 10 / 18%) 0px 0px 16px; } html[data-theme='dark'] .navbar__link--active { - text-shadow: rgb(0 255 133 / 20%) 0px 0px 16px; + text-shadow: rgb(0 255 133 / 20%) 0px 0px 16px; } .navbar__link--active::before { - background-color: var(--ifm-color-primary) !important; + background-color: var(--ifm-color-primary) !important; } html[data-theme='dark'] .navbar__link--active { - --ifm-navbar-link-color: var(--ifm-color-primary); + --ifm-navbar-link-color: var(--ifm-color-primary); } html[data-theme='dark'] .navbar__link--active::before { - background-color: var(--ifm-color-primary) !important; + background-color: var(--ifm-color-primary) !important; } .navbar__brand { - margin-right: 0; - align-items: center; - color: var(--ifm-font-color-base); + margin-right: 0; + align-items: center; + color: var(--ifm-font-color-base); } .navbar__logo { - margin-right: 0; + margin-right: 0; } .navbar__brand { - font-family: 'JetBrains Mono', monospace !important; - font-weight: 300 !important; - margin-right: 1rem; + font-family: 'JetBrains Mono', monospace !important; + font-weight: 300 !important; + margin-right: 1rem; } .navbar__logo path { - fill: #1C1A16; - stroke: #1C1A16; + fill: #1c1a16; + stroke: #1c1a16; } html[data-theme='dark'] .navbar__logo path { - fill: #F0F0EE !important; - stroke: #F0F0EE !important; + fill: #f0f0ee !important; + stroke: #f0f0ee !important; } html[data-theme='dark'] .navbar__logo img { - filter: invert(1) brightness(0.95); + filter: invert(1) brightness(0.95); } .navbar__title { - display: flex; - flex-direction: column; - align-items: flex-start; - line-height: 1.25; + display: flex; + flex-direction: column; + align-items: flex-start; + line-height: 1.25; } .theme-doc-sidebar-container { - margin-right: 1rem; - background-color: var(--ifm-background-color); + margin-right: 1rem; + background-color: var(--ifm-background-color); } -/* Category headers in sidebar */ -.theme-doc-sidebar-item-category > .menu__link { - font-weight: 600; - color: var(--ifm-color-primary); +.rushdb-docs-sidebar-shell { + position: relative; + display: flex; + flex-direction: column; + height: 100vh; +} + +.rushdb-docs-sidebar-header, +.rushdb-docs-sidebar-footer { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1rem; + background: color-mix(in srgb, var(--ifm-background-color) 88%, transparent); + backdrop-filter: blur(10px); + flex: 0 0 auto; +} + +.rushdb-docs-sidebar-header { + position: sticky; + top: 0; + justify-content: space-between; + border-bottom: 1px solid var(--ifm-toc-border-color); + z-index: 2; +} + +.rushdb-docs-sidebar-footer { + position: sticky; + bottom: 0; + flex-direction: column; + align-items: stretch; + gap: 0; + padding: 0; + border-top: 1px solid var(--ifm-toc-border-color); + margin-top: auto; + z-index: 2; +} + +.rushdb-sidebar-community { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem 0.75rem; + padding: 0.625rem 1rem 0.5rem; + border-bottom: 1px solid var(--ifm-toc-border-color); +} + +.rushdb-sidebar-community__link { + font-size: 0.875rem; + color: var(--ifm-color-emphasis-600); + text-decoration: none; + white-space: nowrap; +} + +.rushdb-sidebar-community__link:hover { + color: var(--ifm-color-primary); + text-decoration: none; +} + +.rushdb-sidebar-community__copy { + width: 100%; + font-size: 0.8rem; + color: var(--ifm-color-emphasis-500); + margin-top: 0.1rem; +} + +.rushdb-sidebar-controls { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.875rem 1rem; +} + +/* Hide the Docusaurus page footer */ +.footer { + display: none !important; +} + +.rushdb-docs-sidebar-shell .theme-doc-sidebar-container { + margin-right: 0; +} + +.rushdb-docs-sidebar-shell [class*='sidebarViewport'] { + max-height: 100vh; + height: 100%; +} + +.rushdb-docs-sidebar-shell [class*='sidebar_'] { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.rushdb-docs-sidebar-shell [class*='menu_'] { + padding-top: 0rem; + flex: 1 1 auto; + min-height: 0; +} + +.rushdb-sidebar-cta { + flex: 0 0 auto; + min-width: fit-content; +} + +.rushdb-theme-switch { + position: relative; + display: inline-flex; + align-items: center; + flex-shrink: 0; + width: 52px; + height: 28px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--ifm-color-primary) 32%, transparent); + padding: 0; + cursor: pointer; + outline: none; + transition: + background 0.2s ease, + border-color 0.2s ease, + opacity 0.15s; + background: color-mix(in srgb, var(--ifm-color-primary) 12%, var(--ifm-background-surface-color)); +} + +.rushdb-theme-switch:focus-visible { + box-shadow: 0 0 0 2px var(--ifm-color-primary); +} + +.rushdb-theme-switch:disabled { + opacity: 0.5; + cursor: default; +} + +/* Background icons visible through the track */ +.rushdb-theme-switch__icons { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 6px; + pointer-events: none; +} + +.rushdb-theme-switch__sun { + color: var(--ifm-color-primary); + opacity: 0.9; +} + +.rushdb-theme-switch__moon { + color: var(--ifm-color-emphasis-500); + opacity: 0.8; +} + +/* Sliding thumb */ +.rushdb-theme-switch__thumb { + position: absolute; + top: 2px; + left: 2px; + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--ifm-background-color); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + transition: + left 0.2s ease, + background 0.2s ease; + pointer-events: none; +} + +.rushdb-theme-switch[data-dark='true'] .rushdb-theme-switch__thumb { + left: 26px; +} + +.rushdb-theme-switch__thumb-icon { + color: var(--ifm-color-primary); +} + +html[data-theme='dark'] .rushdb-theme-switch { + background: color-mix(in srgb, var(--ifm-color-primary) 14%, var(--ifm-background-surface-color)); +} + +.rushdb-sidebar-control { + flex: 0 0 auto; + width: 2rem; + height: 2rem; + border: 1px solid var(--ifm-toc-border-color); + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + color: var(--ifm-menu-color); + background: var(--ifm-background-surface-color); +} + +.rushdb-sidebar-control:hover { + color: var(--ifm-font-color-base); + background: var(--ifm-color-emphasis-100); +} + +.rushdb-sidebar-control__icon { + width: 1rem; + height: 1rem; + flex: 0 0 auto; + color: inherit; +} + +.rushdb-sidebar-control__label { + pointer-events: none; +} + +.rushdb-sidebar-expand-rail { + display: none; } -/* Menu items */ +.theme-doc-sidebar-menu .menu__list-item { + margin: 0; +} + +.theme-doc-sidebar-menu .menu__link { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.docRoot_DfVB:has(.rushdb-docs-sidebar-shell--collapsed) .theme-doc-sidebar-container { + width: 56px !important; + min-width: 56px; + border: none !important; +} + +.docRoot_DfVB:has(.rushdb-docs-sidebar-shell--collapsed) [class*='docMainContainer'] { + max-width: calc(100% - 56px); +} + +.docRoot_DfVB:has(.rushdb-docs-sidebar-shell--collapsed) [class*='docItemWrapper'] { + max-width: calc(var(--ifm-container-width) + 56px) !important; +} + +.rushdb-docs-sidebar-shell--collapsed { + overflow: hidden; + width: 56px; + border: none; + box-sizing: border-box; +} + +.rushdb-docs-sidebar-shell--collapsed .rushdb-docs-sidebar-footer, +.rushdb-docs-sidebar-shell--collapsed [class*='sidebar_'] { + display: none; +} + +.rushdb-docs-sidebar-shell--collapsed .rushdb-docs-sidebar-header { + display: flex; + justify-content: flex-start; + padding: 0.875rem 0 0.875rem 1rem; + border-bottom: none; +} + +.rushdb-docs-sidebar-shell--collapsed .rushdb-docs-sidebar-header .navbar__brand { + margin-right: 0; +} + +.rushdb-docs-sidebar-shell--collapsed .rushdb-docs-sidebar-header .navbar__title { + display: none; +} + +.rushdb-docs-sidebar-shell--collapsed .rushdb-sidebar-cta { + visibility: hidden; +} + +.rushdb-docs-sidebar-shell--collapsed .rushdb-sidebar-expand-rail { + position: absolute; + bottom: 0.75rem; + left: 50%; + transform: translateX(-50%); + display: flex; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + border: 1px solid var(--ifm-toc-border-color); + border-radius: 999px; + background: var(--ifm-background-color); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); + z-index: 2; +} + +.rushdb-sidebar-expand-rail__icon { + width: 1rem; + height: 1rem; + flex: 0 0 auto; + color: var(--ifm-menu-color); +} + +html[data-theme='dark'] .rushdb-sidebar-control { + background: var(--ifm-background-surface-color); +} + +html[data-theme='dark'] .rushdb-sidebar-control:hover { + background: var(--ifm-color-emphasis-100); +} + +@media (max-width: 996px) { + :root { + --ifm-navbar-height: 3.75rem; + } + + .navbar { + display: flex !important; + } + + .rushdb-docs-sidebar-shell { + display: contents; + } + + .rushdb-docs-sidebar-header, + .rushdb-docs-sidebar-footer { + display: none; + } + + .rushdb-sidebar-theme-toggle { + display: none; + } +} + +/* ── Sidebar scrollbar — subtle, non-native ──────────────────────────────── */ +@media (pointer: fine) { + .menu.thin-scrollbar { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.15) transparent; + } + + html[data-theme='dark'] .menu.thin-scrollbar { + scrollbar-color: rgba(255, 255, 255, 0.12) transparent; + } + + .menu.thin-scrollbar::-webkit-scrollbar { + width: 4px; + } + + .menu.thin-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + + .menu.thin-scrollbar::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 4px; + } + + html[data-theme='dark'] .menu.thin-scrollbar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.12); + } + + .menu.thin-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.28); + } + + html[data-theme='dark'] .menu.thin-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.25); + } +} + +/* Section header categories (non-collapsible) — non-interactive labels */ +.theme-doc-sidebar-item-category > .menu__list-item-collapsible > .menu__link:not(.menu__link--sublist) { + font-weight: 600; + color: rgba(0, 0, 0, 0.85); + cursor: default !important; + pointer-events: none; +} + +html[data-theme='dark'] + .theme-doc-sidebar-item-category + > .menu__list-item-collapsible + > .menu__link:not(.menu__link--sublist) { + color: #ffffff; +} + +/* Collapsible sub-category titles — bold, interactive */ +.theme-doc-sidebar-item-category > .menu__list-item-collapsible > .menu__link.menu__link--sublist { + font-weight: 600; + color: rgba(0, 0, 0, 0.85); +} + +html[data-theme='dark'] + .theme-doc-sidebar-item-category + > .menu__list-item-collapsible + > .menu__link.menu__link--sublist { + color: #ffffff; +} + +.theme-doc-sidebar-item-category > .menu__list-item-collapsible > .menu__link.menu__link--sublist:hover { + color: var(--ifm-color-primary); + background-color: transparent !important; +} + +html[data-theme='dark'] + .theme-doc-sidebar-item-category + > .menu__list-item-collapsible + > .menu__link.menu__link--sublist:hover { + color: rgba(255, 255, 255, 0.85) !important; + background-color: transparent !important; +} + +/* All menu links — base defaults */ +.menu__link { + font-weight: 400; + font-size: 14px; + background-color: transparent !important; +} + +/* Leaf page links — dimmed to create hierarchy */ .theme-doc-sidebar-item-link > .menu__link { - border-radius: 6px; - transition: all 0.2s ease; + color: rgba(0, 0, 0, 0.5); + border-radius: 6px; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +/* Connect sidebar rows — icon + label on one line */ +.connect-sidebar-link .menu__link { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.connect-sidebar-link__link { + color: rgba(0, 0, 0, 0.56); +} + +html[data-theme='dark'] .connect-sidebar-link__link { + color: rgba(255, 255, 255, 0.54); +} + +.connect-sidebar-link__link:hover { + color: rgba(0, 0, 0, 0.82); + background: rgba(0, 0, 0, 0.03); +} + +html[data-theme='dark'] .connect-sidebar-link__link:hover { + color: rgba(255, 255, 255, 0.82); + background: rgba(255, 255, 255, 0.04); +} + +.connect-sidebar-link__label { + min-width: 0; +} + +.sidebar-link-icon { + flex: 0 0 auto; + color: inherit; +} + +.connect-sidebar-link__link--external::after { + content: ''; + width: 0.85rem; + height: 0.85rem; + margin-left: 0.15rem; + flex: 0 0 auto; + background: currentColor; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6m4-3h6m0 0v6m0-6L10 14'/%3E%3C/svg%3E") + no-repeat center / contain; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6m4-3h6m0 0v6m0-6L10 14'/%3E%3C/svg%3E") + no-repeat center / contain; +} + +html[data-theme='dark'] .theme-doc-sidebar-item-link > .menu__link { + color: rgba(255, 255, 255, 0.5); } .menu__link:hover { - color: var(--ifm-color-primary); + color: var(--ifm-color-primary); } +html[data-theme='dark'] .theme-doc-sidebar-item-link > .menu__link:hover { + color: rgba(255, 255, 255, 0.85); +} .menu__list-item-collapsible:hover { - background-color: transparent !important; - color: var(--ifm-menu-color) !important; + background-color: transparent !important; + color: var(--ifm-menu-color) !important; } -.menu__link { - font-weight: 600; - font-size: 14px; - background-color: transparent !important; +/* Active leaf — full brightness + subtle rounded highlight */ +.menu__list-item > .menu__link--active { + font-weight: 500; + background-color: rgba(0, 0, 0, 0.05) !important; + border-radius: 6px !important; + color: var(--ifm-color-primary); + text-shadow: rgb(200 84 10 / 18%) 0px 0px 16px; } -.menu__list-item > .menu__link--active { - font-weight: 600; - background-color: transparent !important; - border-radius: 0 !important; - text-shadow: rgb(200 84 10 / 18%) 0px 0px 16px; +html[data-theme='dark'] .menu__list-item > .menu__link--active { + color: var(--ifm-color-primary); } /* Category content spacing */ .theme-doc-sidebar-item-category-level-1 { - @apply border-none; + @apply border-none; +} + +/* Extra breathing room between top-level sections */ +.theme-doc-sidebar-item-category-level-1:not(:first-child) { + margin-top: 24px; +} + +/* Children flush with section title — remove the extra left indent */ +.theme-doc-sidebar-item-category-level-1 > .menu__list { + padding-left: 0; +} + +/* Level-3 items (children of sub-categories) — indent and draw hierarchy guide line */ +.theme-doc-sidebar-item-category-level-2 > .menu__list { + margin-left: 18px; + padding-left: 0; + padding-top: 0 !important; + border-left: 1px solid rgba(0, 0, 0, 0.1); +} + +html[data-theme='dark'] .theme-doc-sidebar-item-category-level-2 > .menu__list { + border-left-color: rgba(255, 255, 255, 0.08); } /* Collapse button */ .clean-btn.theme-doc-sidebar-menu-button { - padding: 0.5rem; - border-radius: 6px; + padding: 0.5rem; + border-radius: 6px; } /* Menu arrow icon */ .menu__link--sublist-caret:after { - background-size: 1.25rem 1.25rem; - opacity: 0.6; + background-size: 1.25rem 1.25rem; + opacity: 0.6; } .theme-doc-toc-desktop { - padding: 0rem !important; + padding: 0rem !important; +} + +/* TOC column — no right padding, reaches the viewport right edge */ +@media (min-width: 997px) { + .col.col--3:has(.table-of-contents) { + padding-right: 0; + } } /* ── TOC: match main sidebar style ──────────────────────────────────────── */ .table-of-contents { - padding-left: 0; - font-weight: 600; + padding-left: 0; + font-weight: 400; } /* Remove li margins — padding goes on the instead, like .menu__link */ .table-of-contents li { - margin: 0; + margin: 0; } .table-of-contents li:not(:first-child) { - margin-top: 0.25rem; + margin-top: 0.25rem; } .menu__list-item-collapsible:first-child { - padding-top: 0.25rem; + padding-top: 0.25rem; } .table-of-contents__link { - font-size: 14px; - font-weight: 600; - color: var(--ifm-menu-color); - display: block; - border-radius: 6px; - padding: var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal); - line-height: 1.25; - transition: color var(--ifm-transition-fast) var(--ifm-transition-timing-default); + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.5); + display: block; + border-radius: 6px; + padding: var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal); + line-height: 1.25; + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +html[data-theme='dark'] .table-of-contents__link { + color: rgba(255, 255, 255, 0.5); } .table-of-contents__link:hover, .table-of-contents__link:hover code { - color: var(--ifm-color-primary); - text-decoration: none; - background: transparent; + color: var(--ifm-color-primary); + text-decoration: none; + background: transparent; +} + +html[data-theme='dark'] .table-of-contents__link:hover, +html[data-theme='dark'] .table-of-contents__link:hover code { + color: rgba(255, 255, 255, 0.85); } .table-of-contents__link--active, .table-of-contents__link--active code { - color: var(--ifm-color-primary) !important; - background: transparent; - text-shadow: rgb(200 84 10 / 18%) 0px 0px 16px; + font-weight: 500; + color: var(--ifm-color-primary) !important; + background-color: rgba(0, 0, 0, 0.05); + text-shadow: rgb(200 84 10 / 18%) 0px 0px 16px; +} + +html[data-theme='dark'] .table-of-contents__link--active, +html[data-theme='dark'] .table-of-contents__link--active code { + color: var(--ifm-color-primary) !important; + text-shadow: rgb(0 255 133 / 20%) 0px 0px 16px; } -/* Keep nesting indent but drop the left border line (sidebar uses padding only) */ +/* Drop the default left border line — padding on provides the indent */ .table-of-contents__left-border { - border-left: none; + border-left: none; } .table-of-contents ul { - padding-left: var(--ifm-menu-link-padding-horizontal); + padding-left: var(--ifm-menu-link-padding-horizontal); } code { - background-color: var(--ifm-code-background); - border: 1px solid var(--ifm-toc-border-color); - font-size: inherit; - border-radius: 6px; - font-family: var(--ifm-font-family-monospace), monospace; - font-feature-settings: "liga" 0; - letter-spacing: -0.02em; + background-color: var(--ifm-code-background); + border: 1px solid var(--ifm-toc-border-color); + font-size: inherit; + border-radius: 6px; + font-family: var(--ifm-font-family-monospace), monospace; + font-feature-settings: 'liga' 0; + letter-spacing: -0.02em; } pre { - font-family: var(--ifm-font-family-monospace), monospace; + font-family: var(--ifm-font-family-monospace), monospace; } pre code { - border: none; - font-size: 0.9rem; - line-height: 1.6; + border: none; + font-size: 0.9rem; + line-height: 1.6; } /* Specific styling for inline code */ :not(pre) > code { - padding: 0.1rem 0.2rem; - vertical-align: middle; + padding: 0.1rem 0.2rem; + vertical-align: middle; } - /* Enhance Manrope readability for text content */ .markdown, article, .navbar, .hero, .footer { - font-family: var(--ifm-font-family-base); + font-family: var(--ifm-font-family-base); } /* Make markdown content more readable */ .markdown { - --ifm-h1-font-size: 2.5rem; - --ifm-h2-font-size: 2rem; - --ifm-h3-font-size: 1.5rem; - --ifm-h4-font-size: 1.25rem; + --ifm-h1-font-size: 2.5rem; + --ifm-h2-font-size: 2rem; + --ifm-h3-font-size: 1.5rem; + --ifm-h4-font-size: 1.25rem; } .markdown h1 { - text-shadow: rgb(200 84 10 / 18%) 0px 0px 20px + text-shadow: rgb(200 84 10 / 18%) 0px 0px 20px; } -html[data-theme='dark'] .markdown h1{ - text-shadow: rgb(0 255 133 / 20%) 0px 0px 28px; +html[data-theme='dark'] .markdown h1 { + text-shadow: rgb(0 255 133 / 20%) 0px 0px 28px; } html[data-theme='dark'] .menu__list-item > .menu__link--active { - text-shadow: rgb(0 255 133 / 20%) 0px 0px 16px; + text-shadow: rgb(0 255 133 / 20%) 0px 0px 16px; } html[data-theme='dark'] .table-of-contents__link--active, html[data-theme='dark'] .table-of-contents__link--active code { - text-shadow: rgb(0 255 133 / 20%) 0px 0px 16px; + color: var(--ifm-color-primary) !important; + text-shadow: rgb(0 255 133 / 20%) 0px 0px 16px; } .markdown > h2 { - margin-top: 3rem; - margin-bottom: 1rem; + margin-top: 3rem; + margin-bottom: 1rem; } .markdown > h3 { - margin-top: 2.5rem; - margin-bottom: 1rem; + margin-top: 2.5rem; + margin-bottom: 1rem; } .theme-code-block { - box-shadow: none !important; - border-radius: 8px !important; - overflow: hidden; - border: 1px solid var(--ifm-toc-border-color) !important; + box-shadow: none !important; + border-radius: 8px !important; + overflow: hidden; + border: 1px solid var(--ifm-toc-border-color) !important; } /* Docusaurus Admonition (:::info, :::tip, etc.) custom theming */ .theme-admonition { - border-radius: 0 8px 8px 0; - padding: 1.25rem 1.5rem 1.25rem 1.25rem; - margin: 1.5rem 0; - border-left: 2px solid var(--ifm-color-primary); - background-color: rgba(200, 84, 10, 0.06); - color: var(--ifm-font-color-base); + border-radius: 0 8px 8px 0; + padding: 1.25rem 1.5rem 1.25rem 1.25rem; + margin: 1.5rem 0; + border-left: 2px solid var(--ifm-color-primary); + background-color: rgba(200, 84, 10, 0.06); + color: var(--ifm-font-color-base); } .theme-admonition-info { - border-color: var(--ifm-color-primary); - background-color: rgba(200, 84, 10, 0.07); - color: var(--ifm-font-color-base); + border-color: var(--ifm-color-primary); + background-color: rgba(200, 84, 10, 0.07); + color: var(--ifm-font-color-base); } .theme-admonition .admonition-icon svg { - color: var(--ifm-color-primary); + color: var(--ifm-color-primary); } html[data-theme='dark'] .theme-admonition { - background-color: rgba(0, 255, 133, 0.06); - border-color: var(--ifm-color-primary); - color: var(--ifm-font-color-base); + background-color: rgba(0, 255, 133, 0.06); + border-color: var(--ifm-color-primary); + color: var(--ifm-font-color-base); } html[data-theme='dark'] .theme-admonition-info { - background-color: rgba(0, 255, 133, 0.06); - border-color: var(--ifm-color-primary); - color: var(--ifm-font-color-base); + background-color: rgba(0, 255, 133, 0.06); + border-color: var(--ifm-color-primary); + color: var(--ifm-font-color-base); } html[data-theme='dark'] .theme-admonition .admonition-icon svg { - color: var(--ifm-color-primary); + color: var(--ifm-color-primary); } /*html[data-theme='dark'] > .markdown > p, ul, ol, li {*/ @@ -454,46 +965,49 @@ html[data-theme='dark'] .theme-admonition .admonition-icon svg { /* Improve container width on different screen sizes */ @media (min-width: 1440px) { - .container { -max-width: var(--ifm-container-width-xl) !important; - padding: 0 2rem; - } + .container { + max-width: var(--ifm-container-width-xl) !important; + padding: 0 2rem; + } } .footer__copyright { - padding-top: 2rem; - text-align: left; + padding-top: 2rem; + text-align: left; } .menu { - @apply !pl-4 !pt-2; + @apply !pl-4 !pt-4; } .menu > :first-child:not(.sdk-link) { - @apply pt-2; + @apply pt-2; } .sdk-link { - @apply pt-2; + @apply pt-2; } .header-github-link::before { - content: ''; - width: 24px; - height: 24px; - display: flex; - background-color: var(--ifm-navbar-link-color); - -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); - transition: background-color var(--ifm-transition-fast) + content: ''; + width: 24px; + height: 24px; + display: flex; + background-color: var(--ifm-navbar-link-color); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); + transition: background-color var(--ifm-transition-fast); } .header-github-link:hover::before { - background-color: var(--ifm-navbar-link-hover-color); + background-color: var(--ifm-navbar-link-hover-color); } -.python-sdk, .typescript-sdk, .rest-api, .mcp-server { - @apply flex items-center justify-center gap-2; +.python-sdk, +.typescript-sdk, +.rest-api, +.mcp-server { + @apply flex items-center justify-center gap-2; } .python-sdk > a::before, .typescript-sdk > a::before, @@ -503,432 +1017,485 @@ max-width: var(--ifm-container-width-xl) !important; .navbar__link.typescript-sdk::before, .navbar__link.rest-api::before, .navbar__link.mcp-server::before { - content: ''; - display: flex; - flex-shrink: 0; - background-color: var(--ifm-menu-color); - -webkit-mask-size: contain; - mask-size: contain; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; + content: ''; + display: flex; + flex-shrink: 0; + background-color: var(--ifm-menu-color); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; } /* Sidebar: 18px + right margin */ .python-sdk > a::before, .typescript-sdk > a::before, .rest-api > a::before { - width: 18px; - height: 18px; - @apply mr-2; + width: 18px; + height: 18px; + @apply mr-2; } /* mcp-server: 16px in both contexts */ .mcp-server > a::before, .navbar__link.mcp-server::before { - width: 16px; - height: 16px; + width: 16px; + height: 16px; } .mcp-server > a::before { - @apply mr-2; + @apply mr-2; } /* Navbar: python + typescript = 20px */ .navbar__link.python-sdk::before, .navbar__link.typescript-sdk::before { - width: 20px; - height: 20px; + width: 20px; + height: 20px; } /* Navbar: rest-api = 22px */ .navbar__link.rest-api::before { - width: 22px; - height: 22px; + width: 22px; + height: 22px; } /* Per-technology mask images (sidebar + navbar share the same SVG) */ .python-sdk > a::before, .navbar__link.python-sdk::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E%0A"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E%0A"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E%0A"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E%0A"); } .typescript-sdk > a::before, .navbar__link.typescript-sdk::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E"); + -webkit-mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E'); + mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E'); } .rest-api > a::before, .navbar__link.rest-api::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); } .mcp-server > a::before, .navbar__link.mcp-server::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' height='1em' style='flex:none;line-height:1' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EModelContextProtocol%3C/title%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' height='1em' style='flex:none;line-height:1' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EModelContextProtocol%3C/title%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' height='1em' style='flex:none;line-height:1' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EModelContextProtocol%3C/title%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' height='1em' style='flex:none;line-height:1' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EModelContextProtocol%3C/title%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); } .tutorials-link { - @apply flex items-center justify-center gap-2; + @apply flex items-center justify-center gap-2; } .tutorials-link > a::before, .navbar__link.tutorials-link::before { - content: ''; - display: flex; - flex-shrink: 0; - background-color: var(--ifm-menu-color); - -webkit-mask-size: contain; - mask-size: contain; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - width: 18px; - height: 18px; - @apply mr-2; - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9'/%3E%3Cpath d='m18 15 4-4'/%3E%3Cpath d='m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.113-1.454L9 2.96l.92.82A6 6 0 0 1 12 8.586V10l2 2 1.172-1.172a2 2 0 0 1 2.828 0L21.5 13.5'/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9'/%3E%3Cpath d='m18 15 4-4'/%3E%3Cpath d='m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.113-1.454L9 2.96l.92.82A6 6 0 0 1 12 8.586V10l2 2 1.172-1.172a2 2 0 0 1 2.828 0L21.5 13.5'/%3E%3C/svg%3E"); + content: ''; + display: flex; + flex-shrink: 0; + background-color: var(--ifm-menu-color); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + width: 18px; + height: 18px; + @apply mr-2; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9'/%3E%3Cpath d='m18 15 4-4'/%3E%3Cpath d='m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.113-1.454L9 2.96l.92.82A6 6 0 0 1 12 8.586V10l2 2 1.172-1.172a2 2 0 0 1 2.828 0L21.5 13.5'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9'/%3E%3Cpath d='m18 15 4-4'/%3E%3Cpath d='m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.113-1.454L9 2.96l.92.82A6 6 0 0 1 12 8.586V10l2 2 1.172-1.172a2 2 0 0 1 2.828 0L21.5 13.5'/%3E%3C/svg%3E"); } .navbar__link.tutorials-link::before { - @apply mr-0; - width: 18px; - height: 18px; + @apply mr-0; + width: 18px; + height: 18px; } .navbar__items--right { - @apply gap-4; + @apply gap-4; } /* ── Programming-language tabs: smaller than outer demo tabs ─────────────── */ .language-tabs .tabs { - margin-bottom: 0; - gap: 16px; + margin-bottom: 0; + gap: 16px; } .language-tabs .tabs__item { - font-size: 14px; - font-weight: 400; - font-family: var(--ifm-font-family-monospace); - color: var(--ifm-menu-color); - padding: 0.25rem 0.7rem; - display: inline-flex !important; - align-items: center; - gap: 8px; - border-radius: 4px; - border-bottom: 0px solid transparent; + font-size: 14px; + font-weight: 400; + font-family: var(--ifm-font-family-monospace); + color: var(--ifm-menu-color); + padding: 0.25rem 0.7rem; + display: inline-flex !important; + align-items: center; + gap: 8px; + border-radius: 4px; + border-bottom: 0px solid transparent; } .language-tabs .tabs__item svg { - width: 13px !important; - height: 13px !important; + width: 13px !important; + height: 13px !important; } .language-tabs .tabs__item--active { - border-bottom-color: transparent !important; - background: var(--ifm-hover-overlay) !important; - color: var(--ifm-menu-color-active); + border-bottom-color: transparent !important; + background: var(--ifm-hover-overlay) !important; + color: var(--ifm-menu-color-active); } .tabs__item { - color: var(--ifm-menu-color); + color: var(--ifm-menu-color); } .tabs__item--active { - color: var(--ifm-menu-color-active); + color: var(--ifm-menu-color-active); } -.tabs__item[data-lang="python"]::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E"); +.tabs__item[data-lang='python']::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M511.847,757.279c0,10.586,0,20.188,0,30.832c80.904,0,161.375,0,241.785,0 c0,37.582,0,74.129-0.01,110.672c0,0.328-0.164,0.652-0.221,0.984c-5.341,30.986-22.589,54.121-47.641,72.006 c-30.986,22.119-66.561,32.812-103.044,41.27c-85.164,19.752-168.318,12.871-248.585-21.24 c-19.08-8.107-36.901-20.795-52.854-34.273c-22.846-19.307-33.87-45.564-33.9-75.832c-0.073-78.047-0.805-156.102,0.225-234.133 c0.925-70.191,55.389-122.805,125.531-123.01c77.975-0.227,155.967-1.086,233.918,0.23 C705.86,526.119,772.818,466,783.688,391.142c1.251-8.611,1.377-17.432,1.425-26.162c0.163-30.611,0.064-61.224,0.064-91.837 c0-3.605,0-7.213,0-11.838c12.517,0,23.854,0,35.193,0c18.148,0.002,36.3,0.142,54.448-0.023 c43.185-0.395,75.38,18.446,97.422,55.311c16.096,26.917,24.123,56.681,30.942,86.92c10.478,46.456,16.828,93.305,13.369,141.044 c-4.067,56.15-20.297,108.848-47.128,158.115c-20.56,37.752-53.253,54.877-96.167,54.734 c-115.953-0.381-231.907-0.129-347.859-0.127C521.083,757.279,516.769,757.279,511.847,757.279z M648.96,850.41 c-25.734-0.252-46.009,19.758-46.221,45.611c-0.214,25.793,19.813,46.887,44.912,47.307 c25.027,0.418,46.009-20.428,46.279-45.986C694.203,871.342,674.386,850.658,648.96,850.41z'/%3E%3Cpath fill='white' clip-rule='evenodd' fill-rule='evenodd' d='M510.621,261.305c0-10.549,0-20.083,0-30.812c-81.056,0-161.535,0-242.22,0 c-0.474-3.074-1.038-5.012-1.03-6.947c0.141-34.312-0.533-68.657,0.749-102.928c2.042-54.623,23.93-84.223,76.56-98.907 c31.827-8.878,64.892-14.673,97.789-18.063c64.773-6.675,129.604-4.182,193.803,7.426c37.246,6.734,68.971,24.009,92.947,53.935 c16.724,20.873,25.52,44.756,25.516,71.703c-0.017,77.714,0.374,155.43-0.115,233.142 c-0.449,71.218-53.786,123.692-125.062,123.814c-78.651,0.136-157.304-0.036-235.956,0.057 c-76.784,0.088-139.957,52.139-154.399,127.492c-1.883,9.83-2.224,20.059-2.314,30.111c-0.285,31.285-0.105,62.574-0.105,93.861 c0,3.617,0,7.23,0,12.09c-7.474,0-13.77,0.051-20.063-0.008c-24.864-0.227-49.776,0.426-74.576-0.945 c-39.027-2.16-68.304-21.814-89.938-53.754c-18.086-26.703-25.991-57.164-33.359-88.004 C-0.209,534.822,3.376,455.854,25.584,377.35c20.668-73.063,76.843-115.872,152.937-115.998 c106.549-0.177,213.097-0.047,319.646-0.047C502.121,261.305,506.076,261.305,510.621,261.305z M373.439,75.404 c-25.309,0.176-45.207,20.863-45.057,46.848c0.149,25.682,20.729,46.29,45.985,46.043c25.146-0.245,45.418-21.308,45.201-46.962 C419.35,95.679,398.811,75.23,373.439,75.404z'/%3E%3C/svg%3E"); } -.tabs__item[data-lang="typescript"]::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E"); +.tabs__item[data-lang='typescript']::before { + -webkit-mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E'); + mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20viewBox%3D%220%200%20128%20128%22%3E%3Cpath%20fill%3D%22white%22%20d%3D%22M2%2C63.91v62.5H127V1.41H2Zm100.73-5a15.56%2C15.56%2C0%2C0%2C1%2C7.82%2C4.5%2C20.58%2C20.58%2C0%2C0%2C1%2C3%2C4c0%2C.16-5.4%2C3.81-8.69%2C5.85-.12.08-.6-.44-1.13-1.23a7.09%2C7.09%2C0%2C0%2C0-5.87-3.53c-3.79-.26-6.23%2C1.73-6.21%2C5a4.58%2C4.58%2C0%2C0%2C0%2C.54%2C2.34c.83%2C1.73%2C2.38%2C2.76%2C7.24%2C4.86%2C8.95%2C3.85%2C12.78%2C6.39%2C15.16%2C10%2C2.66%2C4%2C3.25%2C10.46%2C1.45%2C15.24-2%2C5.2-6.9%2C8.73-13.83%2C9.9a38.32%2C38.32%2C0%2C0%2C1-9.52-.1A23%2C23%2C0%2C0%2C1%2C80%2C109.19c-1.15-1.27-3.39-4.58-3.25-4.82a9.34%2C9.34%2C0%2C0%2C1%2C1.15-.73L82.5%2C101l3.59-2.08.75%2C1.11a16.78%2C16.78%2C0%2C0%2C0%2C4.74%2C4.54c4%2C2.1%2C9.46%2C1.81%2C12.16-.62a5.43%2C5.43%2C0%2C0%2C0%2C.69-6.92c-1-1.39-3-2.56-8.59-5-6.45-2.78-9.23-4.5-11.77-7.24a16.48%2C16.48%2C0%2C0%2C1-3.43-6.25%2C25%2C25%2C0%2C0%2C1-.22-8c1.33-6.23%2C6-10.58%2C12.82-11.87A31.66%2C31.66%2C0%2C0%2C1%2C102.73%2C58.93ZM73.39%2C64.15l0%2C5.12H57.16V115.5H45.65V69.26H29.38v-5a49.19%2C49.19%2C0%2C0%2C1%2C.14-5.16c.06-.08%2C10-.12%2C22-.1L73.33%2C59Z%22/%3E%3C/svg%3E'); } -.tabs__item[data-lang="rest"]::before { - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); +.tabs__item[data-lang='rest']::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.0' viewBox='0 0 512.000000 533.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,533.000000) scale(0.100000,-0.100000)' fill='%23000000' stroke='none'%3E%3Cpath d='M1820 4890 c-568 -24 -859 -109 -1082 -319 -174 -164 -276 -384 -322 -703 -34 -231 -40 -424 -40 -1163 0 -875 12 -1082 80 -1350 106 -423 378 -673 829 -764 269 -54 505 -64 1410 -58 762 5 863 9 1080 47 321 56 539 171 691 364 194 245 253 496 275 1171 12 372 5 1283 -11 1500 -36 472 -120 719 -314 923 -199 209 -456 304 -931 343 -166 13 -1395 20 -1665 9z m1442 -320 c534 -22 747 -79 913 -245 144 -143 206 -336 235 -725 16 -219 23 -1113 11 -1475 -21 -656 -74 -864 -262 -1040 -136 -126 -305 -184 -639 -217 -192 -19 -1728 -19 -1920 0 -334 33 -503 91 -639 217 -187 174 -241 384 -262 1025 -12 357 -5 1280 11 1495 20 269 61 451 128 580 49 93 154 198 247 247 164 86 430 131 825 141 296 8 1136 6 1352 -3z'/%3E%3Cpath d='M1525 3491 c-16 -10 -38 -27 -48 -38 -10 -11 -119 -324 -243 -694 -250 -750 -243 -717 -177 -790 65 -71 160 -74 229 -5 23 23 41 61 70 149 l38 117 251 0 250 0 50 -121 c56 -138 94 -182 164 -194 53 -9 126 25 158 74 46 68 46 67 -253 784 -151 364 -285 673 -297 685 -50 54 -135 69 -192 33z m166 -774 l68 -162 -121 -3 c-66 -1 -123 0 -126 3 -4 3 18 80 48 170 30 90 56 162 59 159 3 -2 35 -77 72 -167z'/%3E%3Cpath d='M2700 3501 c-74 -23 -136 -78 -169 -151 -20 -44 -21 -63 -21 -686 l0 -641 23 -34 c32 -49 105 -83 158 -74 53 9 115 64 129 113 5 21 10 146 10 280 l0 242 148 0 c238 0 348 36 455 149 67 70 102 129 121 206 57 216 -39 443 -232 548 -90 49 -146 57 -382 56 -118 0 -226 -4 -240 -8z m466 -327 c49 -23 77 -68 82 -129 5 -63 -13 -102 -65 -142 -36 -28 -39 -28 -195 -31 l-158 -4 0 161 0 161 151 0 c119 0 158 -3 185 -16z'/%3E%3Cpath d='M3890 3502 c-40 -19 -70 -46 -84 -77 -14 -29 -16 -113 -16 -718 l0 -684 23 -34 c32 -49 105 -83 158 -74 54 9 115 64 129 116 7 25 10 266 8 701 -3 642 -4 664 -23 697 -30 52 -67 74 -127 77 -29 2 -60 0 -68 -4z'/%3E%3C/g%3E%3C/svg%3E"); } -.tabs__item[data-lang="mcp-server"]::before, -.tabs__item[data-lang="mcp"]::before { - width: 13px; - height: 13px; - -webkit-mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); +.tabs__item[data-lang='mcp-server']::before, +.tabs__item[data-lang='mcp']::before { + width: 13px; + height: 13px; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg fill='currentColor' fill-rule='evenodd' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z'%3E%3C/path%3E%3Cpath d='M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z'%3E%3C/path%3E%3C/svg%3E"); } /* ── Responsive outer tabs: select on mobile, buttons on desktop ─────────── */ .responsive-tabs__select-wrapper { - display: none; + display: none; } .responsive-tabs__nav { - display: flex; - flex-wrap: wrap; - gap: 4px; - border-bottom: 2px solid var(--ifm-toc-border-color); - padding-bottom: 0; - margin-bottom: 1rem; + display: flex; + flex-wrap: wrap; + gap: 4px; + border-bottom: 2px solid var(--ifm-toc-border-color); + padding-bottom: 0; + margin-bottom: 1rem; } .responsive-tabs__tab { - background: none; - border: none; - border-bottom: 2px solid transparent; - margin-bottom: -2px; - padding: 0.5rem 1rem; - font-size: 0.9rem; - font-weight: 600; - font-family: var(--ifm-font-family-base); - color: var(--ifm-menu-color); - cursor: pointer; - border-radius: 6px 6px 0 0; - transition: color 0.15s, background 0.15s; + background: none; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + padding: 0.5rem 1rem; + font-size: 0.9rem; + font-weight: 600; + font-family: var(--ifm-font-family-base); + color: var(--ifm-menu-color); + cursor: pointer; + border-radius: 6px 6px 0 0; + transition: + color 0.15s, + background 0.15s; } .responsive-tabs__tab:hover { - background: var(--ifm-hover-overlay); - color: var(--ifm-color-primary); + background: var(--ifm-hover-overlay); + color: var(--ifm-color-primary); } .responsive-tabs__tab--active { - color: var(--ifm-color-primary) !important; - border-bottom-color: var(--ifm-color-primary) !important; + color: var(--ifm-color-primary) !important; + border-bottom-color: var(--ifm-color-primary) !important; } @media (max-width: 996px) { - .responsive-tabs__nav { - display: none; - } - - .responsive-tabs__select-wrapper { - display: block; - margin-bottom: 1rem; - } - - .responsive-tabs__select { - width: 100%; - padding: 0.5rem 2.25rem 0.5rem 0.75rem; - font-size: 0.9rem; - font-weight: 600; - font-family: var(--ifm-font-family-base); - color: var(--ifm-font-color-base); - background-color: var(--ifm-background-color); - border: 1px solid var(--ifm-toc-border-color); - border-radius: 6px; - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.65rem center; - cursor: pointer; - } - - .responsive-tabs__select:focus { - outline: 2px solid var(--ifm-color-primary); - outline-offset: 2px; - } + .responsive-tabs__nav { + display: none; + } + + .responsive-tabs__select-wrapper { + display: block; + margin-bottom: 1rem; + } + + .responsive-tabs__select { + width: 100%; + padding: 0.5rem 2.25rem 0.5rem 0.75rem; + font-size: 0.9rem; + font-weight: 600; + font-family: var(--ifm-font-family-base); + color: var(--ifm-font-color-base); + background-color: var(--ifm-background-color); + border: 1px solid var(--ifm-toc-border-color); + border-radius: 6px; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.65rem center; + cursor: pointer; + } + + .responsive-tabs__select:focus { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 2px; + } } .cta-button { - border-radius: 0 !important; - font-family: var(--ifm-font-family-monospace); - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 700; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - transition: background-color 0.15s ease; - background-color: #C8540A; - color: #F5F2EB !important; - text-decoration: none !important; + border-radius: 0 !important; + font-family: var(--ifm-font-family-monospace); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + transition: background-color 0.15s ease; + background-color: #c8540a; + color: #f5f2eb !important; + text-decoration: none !important; } .cta-button:hover { - background-color: #b04808; - color: #F5F2EB !important; - text-decoration: none !important; + background-color: #b04808; + color: #f5f2eb !important; + text-decoration: none !important; } html[data-theme='dark'] .cta-button { - background-color: #00FF85 !important; - color: #0A0A0B !important; + background-color: #00ff85 !important; + color: #0a0a0b !important; } html[data-theme='dark'] .cta-button:hover { - background-color: #00e876 !important; - color: #0A0A0B !important; + background-color: #00e876 !important; + color: #0a0a0b !important; } /* Move theme toggle left of Login button */ .navbar__items--right [class*='colorModeToggle'] { - order: 1; + order: 1; } .navbar__items--right .cta-button { - order: 2; + order: 2; } -.cta-button [class*="iconExternalLink"] { - display: none; +.cta-button [class*='iconExternalLink'] { + display: none; } .navbar-sidebar { - height: 100dvh; + display: none !important; + height: 100dvh; +} + +.navbar-sidebar__backdrop { + display: none !important; +} + +@media (max-width: 996px) { + .navbar-sidebar, + .navbar-sidebar__backdrop { + display: block !important; + } + + .navbar-sidebar { + --ifm-navbar-sidebar-width: 100vw; + width: 100vw; + background: var(--ifm-background-color); + } + + .navbar-sidebar__brand, + .navbar-sidebar__item { + background: var(--ifm-background-color); + } + + .navbar__items > .tutorials-link { + display: none !important; + } + + .navbar__items--right [class*='colorModeToggle'] { + display: block !important; + } + + .navbar-sidebar__back { + display: none; + } + + .rushdb-sidebar-community--mobile { + margin-top: 1rem; + border-top: 1px solid var(--ifm-toc-border-color); + } } /* ── Copy page button ──────────────────────────────────────────────────── */ -.breadcrumbs-with-copy { - display: flex; - align-items: center; - justify-content: space-between; - align-items: baseline; - margin-bottom: 24px; - padding-top: 16px; +.doc-actions { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 20px; + padding-top: 16px; +} + +.doc-section-label { + font-size: 13px; + font-weight: 500; + color: rgba(0, 0, 0, 0.35); + letter-spacing: 0.04em; + margin-right: auto; } -.breadcrumbs-actions { - display: inline-flex; - align-items: center; - gap: 6px; - flex-shrink: 0; +html[data-theme='dark'] .doc-section-label { + color: rgba(255, 255, 255, 0.3); } .copy-page-container { - position: relative; - display: inline-flex; - user-select: none; + position: relative; + display: inline-flex; + user-select: none; } .copy-page-main-btn, .copy-page-chevron-btn { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 7px 14px; - background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-300); - font-size: 13.5px; - font-weight: 500; - cursor: pointer; - color: var(--ifm-font-color-base); - line-height: 1; - transition: background 0.12s; - font-family: var(--ifm-font-family-base); + display: inline-flex; + align-items: center; + gap: 5px; + padding: 7px 14px; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + font-size: 13.5px; + font-weight: 500; + cursor: pointer; + color: var(--ifm-font-color-base); + line-height: 1; + transition: background 0.12s; + font-family: var(--ifm-font-family-base); } .copy-page-main-btn { - border-right: none; - border-radius: 6px 0 0 6px; + border-right: none; + border-radius: 6px 0 0 6px; } .copy-page-chevron-btn { - padding: 7px 9px; - border-left: 1px solid var(--ifm-color-emphasis-200); - border-radius: 0 6px 6px 0; + padding: 7px 9px; + border-left: 1px solid var(--ifm-color-emphasis-200); + border-radius: 0 6px 6px 0; } .copy-page-main-btn:hover, .copy-page-chevron-btn:hover { - background: var(--ifm-color-emphasis-100); + background: var(--ifm-color-emphasis-100); } .copy-page-dropdown { - position: absolute; - top: calc(100% + 6px); - right: 0; - min-width: 252px; - background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-300); - border-radius: 10px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13); - overflow: hidden; - z-index: 200; + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 252px; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13); + overflow: hidden; + z-index: 200; } .copy-page-item { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 14px; - text-decoration: none !important; - color: var(--ifm-font-color-base) !important; - transition: background 0.1s; - cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + text-decoration: none !important; + color: var(--ifm-font-color-base) !important; + transition: background 0.1s; + cursor: pointer; } .copy-page-item:hover { - background: var(--ifm-color-emphasis-100); - text-decoration: none !important; + background: var(--ifm-color-emphasis-100); + text-decoration: none !important; } .copy-page-item:hover .copy-page-item-label, .copy-page-item:hover .copy-page-item-desc { - text-decoration: none !important; + text-decoration: none !important; } .copy-page-item--bordered { - border-top: 1px solid var(--ifm-color-emphasis-200); + border-top: 1px solid var(--ifm-color-emphasis-200); } .copy-page-item-icon { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - min-width: 36px; - border-radius: 8px; - background: var(--ifm-color-emphasis-200); - flex-shrink: 0; - color: var(--ifm-font-color-base); - overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + min-width: 36px; + border-radius: 8px; + background: var(--ifm-color-emphasis-200); + flex-shrink: 0; + color: var(--ifm-font-color-base); + overflow: hidden; } .copy-page-item-text { - display: flex; - flex-direction: column; - gap: 2px; + display: flex; + flex-direction: column; + gap: 2px; } .copy-page-item-label { - font-size: 13px; - font-weight: 500; - line-height: 1.2; - color: var(--ifm-font-color-base); - text-decoration: none !important; + font-size: 13px; + font-weight: 500; + line-height: 1.2; + color: var(--ifm-font-color-base); + text-decoration: none !important; } .copy-page-item-desc { - font-size: 11.5px; - line-height: 1.3; - color: var(--ifm-color-emphasis-700); - text-decoration: none !important; + font-size: 11.5px; + line-height: 1.3; + color: var(--ifm-color-emphasis-700); + text-decoration: none !important; } -/* ── TOC column width: fixed 380px ──────────────────────────────────────── */ +/* ── TOC column width: fixed 380px, pushed to the right viewport edge ─────── */ @media (min-width: 997px) { + /* Docs container: full-width so TOC reaches the right viewport edge */ + [class*='docItemWrapper'] { + max-width: 100% !important; + padding-right: 0 !important; + } + .col.col--3:has(.table-of-contents) { max-width: 320px !important; flex: 0 0 320px !important; width: 320px !important; + margin-left: auto; } /* Content column takes the remaining space */ @@ -964,27 +1531,27 @@ html[data-theme='dark'] .cta-button:hover { /* ── Widescreen toggle button ────────────────────────────────────────────── */ .widescreen-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 7px 9px; - background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-300); - border-radius: 6px; - cursor: pointer; - color: var(--ifm-font-color-base); - line-height: 1; - transition: background 0.12s; - font-family: var(--ifm-font-family-base); + display: inline-flex; + align-items: center; + justify-content: center; + padding: 7px 9px; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + cursor: pointer; + color: var(--ifm-font-color-base); + line-height: 1; + transition: background 0.12s; + font-family: var(--ifm-font-family-base); } .widescreen-btn:hover { - background: var(--ifm-color-emphasis-100); + background: var(--ifm-color-emphasis-100); } .widescreen-btn--active { - background: var(--ifm-color-emphasis-200); - border-color: var(--ifm-color-emphasis-400); + background: var(--ifm-color-emphasis-200); + border-color: var(--ifm-color-emphasis-400); } /* ── Widescreen mode overrides ───────────────────────────────────────────── */ @@ -1000,75 +1567,148 @@ html[data-theme='dark'] .cta-button:hover { max-width: none !important; } - /* Keep specific landing pages containered even if widescreen is enabled */ - html[data-widescreen='true'] [class*='docMainContainer']:has(.doc-layout--force-container) .container { - max-width: 1140px !important; - } + /* Keep specific landing pages containered even if widescreen is enabled */ + html[data-widescreen='true'] [class*='docMainContainer']:has(.doc-layout--force-container) .container { + max-width: 1140px !important; + } - html[data-widescreen='true'] [class*='docItemWrapper']:has(.doc-layout--force-container) { - max-width: 1140px !important; - margin: 0 auto; - width: 100%; - } + html[data-widescreen='true'] [class*='docItemWrapper']:has(.doc-layout--force-container) { + max-width: 1140px !important; + margin: 0 auto; + width: 100%; + } } /* ── Markdown tables ─────────────────────────────────────────────────────── */ +.markdown-table-wrapper { + overflow-x: auto; + margin: 1.5rem 0; + border-radius: 8px; + border: 1px solid var(--ifm-toc-border-color); +} + .markdown table { - display: table; - border-collapse: separate; - border-spacing: 0; - width: 100%; - border-radius: 8px; - overflow: hidden; - border: 1px solid var(--ifm-toc-border-color); - font-size: 0.875rem; - margin: 1.5rem 0; + display: table; + border-collapse: separate; + border-spacing: 0; + width: auto; + min-width: 100%; + font-size: 0.875rem; + margin: 0; + border: none; + border-radius: 0; } .markdown table thead tr { - background-color: rgba(0, 0, 0, 0.04); + background-color: rgba(0, 0, 0, 0.04); } html[data-theme='dark'] .markdown table thead tr { - background-color: rgba(255, 255, 255, 0.06); + background-color: rgba(255, 255, 255, 0.06); } .markdown table th { - font-weight: 600; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--ifm-font-color-base); - opacity: 0.65; - padding: 0.7rem 1rem; - border: none; - border-bottom: 1px solid var(--ifm-toc-border-color); - white-space: nowrap; - text-align: left; + font-weight: 600; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ifm-font-color-base); + opacity: 0.65; + padding: 0.7rem 1rem; + border: none; + border-bottom: 1px solid var(--ifm-toc-border-color); + white-space: nowrap; + text-align: left; } .markdown table tr { - font-size: 1rem; - + font-size: 1rem; } .markdown table td { - padding: 0.65rem 1rem; - border: none; - border-bottom: 1px solid var(--ifm-toc-border-color); - vertical-align: top; - line-height: 1.55; + padding: 0.65rem 1rem; + border: none; + border-bottom: 1px solid var(--ifm-toc-border-color); + vertical-align: top; + line-height: 1.55; } .markdown table tbody tr:last-child td { - border-bottom: none; + border-bottom: none; } .markdown table tbody tr:nth-child(even) { - background-color: rgba(0, 0, 0, 0.015); + background-color: rgba(0, 0, 0, 0.015); } html[data-theme='dark'] .markdown table tbody tr:nth-child(even) { - background-color: rgba(255, 255, 255, 0.015); + background-color: rgba(255, 255, 255, 0.015); +} + +/* ── Sidebar category icons ─────────────────────────────────────────────── */ + +/* Make the link a flex row so the icon aligns with the label */ +.menu__link { + display: flex; + align-items: center; + gap: 0; +} + +.sidebar-category-icon { + flex-shrink: 0; + margin-right: 7px; + opacity: 0.6; + color: var(--ifm-menu-color); + transition: + opacity 0.2s ease, + color 0.2s ease; +} + +/* Brighten icon when the link is active or hovered */ +.menu__link:hover .sidebar-category-icon, +.menu__link--active .sidebar-category-icon { + opacity: 1; + color: var(--ifm-menu-color-active); +} + +/* Section header icons — stay static (pointer-events: none prevents hover anyway) */ +.theme-doc-sidebar-item-category + > .menu__list-item-collapsible + > .menu__link:not(.menu__link--sublist):hover + .sidebar-category-icon { + opacity: 0.6; + color: var(--ifm-menu-color); +} + +/* Slightly dim nested (level-2+) category icons */ +.menu__list .menu__list .sidebar-category-icon { + opacity: 0.5; + width: 13px; + height: 13px; +} + +.menu__list .menu__list .menu__link:hover .sidebar-category-icon, +.menu__list .menu__list .menu__link--active .sidebar-category-icon { + opacity: 0.85; +} + +/* Nested section header icons — stay static on hover */ +.menu__list + .menu__list + .theme-doc-sidebar-item-category + > .menu__list-item-collapsible + > .menu__link:not(.menu__link--sublist):hover + .sidebar-category-icon { + opacity: 0.5; + color: var(--ifm-menu-color); +} + +/* Vertical hierarchy line — leaf-item groups under sub-category headers */ +.menu__list .menu__list .menu__list .menu__list { + border-left: 1px solid rgba(0, 0, 0, 0.1); +} + +html[data-theme='dark'] .menu__list .menu__list .menu__list .menu__list { + border-left-color: rgba(255, 255, 255, 0.08); } diff --git a/docs/src/theme/ColorModeToggle/index.tsx b/docs/src/theme/ColorModeToggle/index.tsx new file mode 100644 index 00000000..445ea34a --- /dev/null +++ b/docs/src/theme/ColorModeToggle/index.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import ThemeSwitch from '@site/src/components/ThemeSwitch' +import type { Props } from '@theme/ColorModeToggle' + +export default function ColorModeToggle({ value, onChange, className }: Props) { + const isDark = value === 'dark' + + return ( + onChange(isDark ? 'light' : 'dark')} /> + ) +} diff --git a/docs/src/theme/ColorModeToggle/styles.module.css b/docs/src/theme/ColorModeToggle/styles.module.css new file mode 100644 index 00000000..4c5275cb --- /dev/null +++ b/docs/src/theme/ColorModeToggle/styles.module.css @@ -0,0 +1,88 @@ +/* ── Track ─────────────────────────────────────────────────────────────── */ + +.track { + position: relative; + display: inline-flex; + align-items: center; + width: 44px; + height: 24px; + border-radius: 999px; + border: none; + padding: 0; + cursor: pointer; + flex-shrink: 0; + transition: background 0.2s ease; + outline: none; +} + +.track:focus-visible { + box-shadow: 0 0 0 2px var(--ifm-color-primary); +} + +.trackLight { + background: #e2e8f0; +} + +.trackDark { + background: #2d3748; +} + +/* ── Background icons (visible through track) ──────────────────────────── */ + +.icons { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 5px; + pointer-events: none; +} + +.sun { + color: #f59e0b; + opacity: 0.9; +} + +.moon { + color: #94a3b8; + opacity: 0.9; +} + +/* ── Thumb ─────────────────────────────────────────────────────────────── */ + +.thumb { + position: absolute; + top: 3px; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + transition: + left 0.2s ease, + background 0.2s ease; + pointer-events: none; +} + +.thumbLight { + left: 3px; + background: #fff; +} + +.thumbDark { + left: 23px; + background: #1e293b; +} + +/* ── Thumb icon ────────────────────────────────────────────────────────── */ + +.thumbIcon { + color: #f59e0b; +} + +.thumbDark .thumbIcon { + color: #94a3b8; +} diff --git a/docs/src/theme/DocBreadcrumbs/index.tsx b/docs/src/theme/DocBreadcrumbs/index.tsx index 2c717672..e05248f2 100644 --- a/docs/src/theme/DocBreadcrumbs/index.tsx +++ b/docs/src/theme/DocBreadcrumbs/index.tsx @@ -1,30 +1,15 @@ import React from 'react' -import { useDoc } from '@docusaurus/plugin-content-docs/client' -import DocBreadcrumbs from '@theme-original/DocBreadcrumbs' -import type DocBreadcrumbsType from '@theme/DocBreadcrumbs' -import type { WrapperProps } from '@docusaurus/types' -import CopyPageButton, { WidescreenButton } from '@site/src/components/CopyPageButton' +import { useSidebarBreadcrumbs } from '@docusaurus/plugin-content-docs/client' +import CopyPageButton from '@site/src/components/CopyPageButton' -type Props = WrapperProps -type PageFrontMatter = { - hide_breadcrumbs?: boolean -} - -export default function DocBreadcrumbsWrapper(props: Props) { - const { frontMatter } = useDoc() - const pageFrontMatter = frontMatter as typeof frontMatter & PageFrontMatter - - if (pageFrontMatter.hide_breadcrumbs) { - return null - } +export default function DocBreadcrumbsWrapper() { + const breadcrumbs = useSidebarBreadcrumbs() + const sectionName = breadcrumbs?.[0]?.label return ( -
- -
- - -
+
+ {sectionName && {sectionName}} +
) } diff --git a/docs/src/theme/DocItem/Layout/index.tsx b/docs/src/theme/DocItem/Layout/index.tsx index 09610332..bb1df96d 100644 --- a/docs/src/theme/DocItem/Layout/index.tsx +++ b/docs/src/theme/DocItem/Layout/index.tsx @@ -7,12 +7,17 @@ import type { WrapperProps } from '@docusaurus/types' type Props = WrapperProps type PageFrontMatter = { force_container?: boolean + overview_page?: boolean } export default function DocItemLayoutWrapper(props: Props) { const { frontMatter } = useDoc() const pageFrontMatter = frontMatter as typeof frontMatter & PageFrontMatter + if (pageFrontMatter.overview_page) { + return
{props.children}
+ } + if (pageFrontMatter.force_container) { return (
diff --git a/docs/src/theme/DocSidebar/Desktop/index.tsx b/docs/src/theme/DocSidebar/Desktop/index.tsx new file mode 100644 index 00000000..2ac2d06e --- /dev/null +++ b/docs/src/theme/DocSidebar/Desktop/index.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react' +import Link from '@docusaurus/Link' +import { useColorMode, useThemeConfig } from '@docusaurus/theme-common' +import type { WrapperProps } from '@docusaurus/types' +import DocSidebarDesktop from '@theme-original/DocSidebar/Desktop' +import Logo from '@theme/Logo' +import SidebarCommunity from '@site/src/components/SidebarCommunity' +import ThemeSwitch from '@site/src/components/ThemeSwitch' +import { PanelLeftClose, PanelLeftOpen } from 'lucide-react' +import type DocSidebarDesktopType from '@theme/DocSidebar/Desktop' + +type Props = WrapperProps + +function SidebarThemeToggle() { + const { + colorMode: currentColorMode, + setColorMode, + colorModeChoice + } = useColorMode() as ReturnType & { + colorModeChoice: 'light' | 'dark' | null + } + const { + colorMode: { respectPrefersColorScheme, disableSwitch } + } = useThemeConfig() as { + colorMode: { respectPrefersColorScheme?: boolean; disableSwitch?: boolean } + } + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + if (disableSwitch) { + return null + } + + const nextMode = (() => { + if (!respectPrefersColorScheme) { + return currentColorMode === 'dark' ? 'light' : 'dark' + } + + switch (colorModeChoice) { + case null: + return 'light' + case 'light': + return 'dark' + case 'dark': + return null + default: + return 'light' + } + })() + + const isDark = currentColorMode === 'dark' + + return ( + setColorMode(isDark ? 'light' : 'dark')} /> + ) +} + +function SidebarCollapseToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) { + return ( + + ) +} + +export default function DocSidebarDesktopWrapper(props: Props) { + const [collapsed, setCollapsed] = useState(false) + + useEffect(() => { + const savedState = window.localStorage.getItem('rushdb-docs-sidebar-collapsed') + setCollapsed(savedState === 'true') + }, []) + + useEffect(() => { + window.localStorage.setItem('rushdb-docs-sidebar-collapsed', String(collapsed)) + }, [collapsed]) + + return ( +
+
+ + + GET API KEY + +
+ + + +
+ +
+ + setCollapsed((current) => !current)} /> +
+
+ + +
+ ) +} diff --git a/docs/src/theme/DocSidebarItem/Category/index.tsx b/docs/src/theme/DocSidebarItem/Category/index.tsx new file mode 100644 index 00000000..02dc5210 --- /dev/null +++ b/docs/src/theme/DocSidebarItem/Category/index.tsx @@ -0,0 +1,236 @@ +/** + * Swizzled DocSidebarItem/Category component. + * Adds Lucide icon support via `customProps.icon` on sidebar category items. + * + * Usage in sidebars.ts: + * { type: 'category', label: 'Deploy', customProps: { icon: 'Server' }, ... } + */ +import React, { useEffect, useMemo, type ReactNode } from 'react' +import clsx from 'clsx' +import { + ThemeClassNames, + useThemeConfig, + usePrevious, + Collapsible, + useCollapsible +} from '@docusaurus/theme-common' +import { isSamePath } from '@docusaurus/theme-common/internal' +import { + isActiveSidebarItem, + findFirstSidebarItemLink, + useDocSidebarItemsExpandedState +} from '@docusaurus/plugin-content-docs/client' +import Link from '@docusaurus/Link' +import { translate } from '@docusaurus/Translate' +import useIsBrowser from '@docusaurus/useIsBrowser' +import DocSidebarItems from '@theme/DocSidebarItems' +import * as LucideIcons from 'lucide-react' +import type { LucideProps } from 'lucide-react' + +type LucideIconComponent = React.ComponentType + +function SidebarIcon({ name }: { name: string }): ReactNode { + const IconComponent = (LucideIcons as Record)[name] as LucideIconComponent | undefined + if (!IconComponent) return null + return