recall-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ QDRANT_HOST=localhost
2
+ QDRANT_PORT=6333
3
+ COLLECTION_NAME=memories
4
+ OLLAMA_BASE_URL=http://localhost:11434
5
+ EMBED_MODEL=nomic-embed-text
6
+ OLLAMA_EMBED_PATH=/api/embed
7
+ API_HOST=127.0.0.1
8
+ API_PORT=8100
9
+ API_AUTH_TOKEN=
10
+ MAX_TEXT_LENGTH=8000
11
+ MAX_BATCH_SIZE=100
12
+ HEALTH_CHECK_TIMEOUT_S=5
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .pytest_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anel Canto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,40 @@
1
+ .PHONY: up down serve dev test test-integration test-degraded test-all status install
2
+
3
+ up:
4
+ docker run -d --name qdrant \
5
+ -p 127.0.0.1:6333:6333 \
6
+ -p 127.0.0.1:6334:6334 \
7
+ -v qdrant_data:/qdrant/storage \
8
+ qdrant/qdrant:v1.13.2 || docker start qdrant
9
+
10
+ down:
11
+ docker stop qdrant && docker rm qdrant
12
+
13
+ serve:
14
+ uv run uvicorn memory_api.main:app --host 127.0.0.1 --port 8100
15
+
16
+ dev:
17
+ $(MAKE) up && $(MAKE) serve
18
+
19
+ test:
20
+ uv run pytest tests/test_unit.py -v
21
+
22
+ test-integration:
23
+ uv run pytest tests/test_integration.py -v
24
+
25
+ test-degraded:
26
+ uv run pytest tests/test_degraded.py -v
27
+
28
+ test-all:
29
+ uv run pytest tests/ -v
30
+
31
+ status:
32
+ @curl -sf http://127.0.0.1:8100/health | python3 -m json.tool || echo "API not running"
33
+
34
+ install:
35
+ @mkdir -p ~/.memories
36
+ @test -f ~/.memories/.env || cp .env.example ~/.memories/.env
37
+ uv sync
38
+ @echo ""
39
+ @echo "Installed. Run 'make dev' to start Qdrant + API server."
40
+ @echo "CLI available via: uv run recall --help"
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: recall-cli
3
+ Version: 0.1.0
4
+ Summary: A personal semantic memory system — store, search, and manage memories locally with vector embeddings
5
+ Project-URL: Homepage, https://github.com/anelcanto/recall
6
+ Project-URL: Repository, https://github.com/anelcanto/recall
7
+ Project-URL: Issues, https://github.com/anelcanto/recall/issues
8
+ Author-email: Anel Canto <anelcanto@users.noreply.github.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: fastapi
23
+ Requires-Dist: httpx
24
+ Requires-Dist: pydantic-settings
25
+ Requires-Dist: python-dotenv
26
+ Requires-Dist: qdrant-client
27
+ Requires-Dist: rich
28
+ Requires-Dist: typer
29
+ Requires-Dist: uvicorn[standard]
30
+ Description-Content-Type: text/markdown
31
+
32
+ # recall
33
+
34
+ A personal semantic memory system. Store, search, and manage memories locally using vector embeddings.
35
+
36
+ Everything runs on your machine: FastAPI server, Qdrant vector database (Docker), and Ollama for local embeddings. Zero cost, full privacy.
37
+
38
+ ## Install
39
+
40
+ ### From PyPI
41
+
42
+ ```bash
43
+ pip install recall-cli
44
+ ```
45
+
46
+ ### From Homebrew
47
+
48
+ ```bash
49
+ brew tap anelcanto/recall-cli
50
+ brew install recall-cli
51
+ ```
52
+
53
+ ### From source
54
+
55
+ ```bash
56
+ git clone https://github.com/anelcanto/recall.git
57
+ cd recall
58
+ ./install.sh
59
+ ```
60
+
61
+ Or the quick version:
62
+
63
+ ```bash
64
+ make install
65
+ ```
66
+
67
+ ## Prerequisites
68
+
69
+ - **Docker** — runs Qdrant vector database
70
+ - **Ollama** — local embeddings (`brew install ollama && ollama pull nomic-embed-text`)
71
+ - **uv** — Python package manager (`brew install uv`)
72
+
73
+ ## Quick start
74
+
75
+ ```bash
76
+ # Start services
77
+ make up # Start Qdrant in Docker
78
+ make serve # Start API server (keep this running)
79
+
80
+ # In another terminal
81
+ recall add "The quick brown fox" --tag test
82
+ recall search "fox"
83
+ recall list
84
+ recall status
85
+ ```
86
+
87
+ ## CLI
88
+
89
+ ```
90
+ recall add "text" --tag work --source cli [--dedupe-key "..."]
91
+ recall search "query" --top-k 10 [--no-text] [--output table|json]
92
+ recall ingest <file> [--format lines|jsonl] [--source name] [--auto-dedupe]
93
+ recall list [--limit 20] [--cursor ...] [--output table|json]
94
+ recall delete <id>
95
+ recall status
96
+ ```
97
+
98
+ ### Environment variables
99
+
100
+ | Variable | Default | Description |
101
+ |----------|---------|-------------|
102
+ | `RECALL_API_URL` | `http://127.0.0.1:8100` | API server URL |
103
+ | `RECALL_API_TOKEN` | (none) | Bearer token for auth |
104
+
105
+ ## Architecture
106
+
107
+ ```
108
+ recall CLI --> FastAPI server (:8100) --> Qdrant (Docker :6333)
109
+ |
110
+ v
111
+ Ollama (:11434)
112
+ nomic-embed-text
113
+ ```
114
+
115
+ - **FastAPI** serves the HTTP API
116
+ - **Qdrant** stores vectors and payloads
117
+ - **Ollama** generates embeddings locally using `nomic-embed-text`
118
+ - **CLI** talks to the API over HTTP
119
+
120
+ User config lives in `~/.memories/.env`. Qdrant data persists in a Docker volume.
121
+
122
+ ## API endpoints
123
+
124
+ | Method | Path | Description |
125
+ |--------|------|-------------|
126
+ | `POST` | `/memory` | Store a memory |
127
+ | `POST` | `/search` | Semantic search |
128
+ | `POST` | `/ingest` | Batch import |
129
+ | `GET` | `/memories` | List with pagination |
130
+ | `DELETE` | `/memory/{id}` | Delete a memory |
131
+ | `GET` | `/health` | Service health check |
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ make test # Unit tests (no services needed)
137
+ make test-integration # Integration tests (Qdrant + Ollama required)
138
+ make test-degraded # Degraded mode tests (Qdrant only)
139
+ make test-all # All tests
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,113 @@
1
+ # recall
2
+
3
+ A personal semantic memory system. Store, search, and manage memories locally using vector embeddings.
4
+
5
+ Everything runs on your machine: FastAPI server, Qdrant vector database (Docker), and Ollama for local embeddings. Zero cost, full privacy.
6
+
7
+ ## Install
8
+
9
+ ### From PyPI
10
+
11
+ ```bash
12
+ pip install recall-cli
13
+ ```
14
+
15
+ ### From Homebrew
16
+
17
+ ```bash
18
+ brew tap anelcanto/recall-cli
19
+ brew install recall-cli
20
+ ```
21
+
22
+ ### From source
23
+
24
+ ```bash
25
+ git clone https://github.com/anelcanto/recall.git
26
+ cd recall
27
+ ./install.sh
28
+ ```
29
+
30
+ Or the quick version:
31
+
32
+ ```bash
33
+ make install
34
+ ```
35
+
36
+ ## Prerequisites
37
+
38
+ - **Docker** — runs Qdrant vector database
39
+ - **Ollama** — local embeddings (`brew install ollama && ollama pull nomic-embed-text`)
40
+ - **uv** — Python package manager (`brew install uv`)
41
+
42
+ ## Quick start
43
+
44
+ ```bash
45
+ # Start services
46
+ make up # Start Qdrant in Docker
47
+ make serve # Start API server (keep this running)
48
+
49
+ # In another terminal
50
+ recall add "The quick brown fox" --tag test
51
+ recall search "fox"
52
+ recall list
53
+ recall status
54
+ ```
55
+
56
+ ## CLI
57
+
58
+ ```
59
+ recall add "text" --tag work --source cli [--dedupe-key "..."]
60
+ recall search "query" --top-k 10 [--no-text] [--output table|json]
61
+ recall ingest <file> [--format lines|jsonl] [--source name] [--auto-dedupe]
62
+ recall list [--limit 20] [--cursor ...] [--output table|json]
63
+ recall delete <id>
64
+ recall status
65
+ ```
66
+
67
+ ### Environment variables
68
+
69
+ | Variable | Default | Description |
70
+ |----------|---------|-------------|
71
+ | `RECALL_API_URL` | `http://127.0.0.1:8100` | API server URL |
72
+ | `RECALL_API_TOKEN` | (none) | Bearer token for auth |
73
+
74
+ ## Architecture
75
+
76
+ ```
77
+ recall CLI --> FastAPI server (:8100) --> Qdrant (Docker :6333)
78
+ |
79
+ v
80
+ Ollama (:11434)
81
+ nomic-embed-text
82
+ ```
83
+
84
+ - **FastAPI** serves the HTTP API
85
+ - **Qdrant** stores vectors and payloads
86
+ - **Ollama** generates embeddings locally using `nomic-embed-text`
87
+ - **CLI** talks to the API over HTTP
88
+
89
+ User config lives in `~/.memories/.env`. Qdrant data persists in a Docker volume.
90
+
91
+ ## API endpoints
92
+
93
+ | Method | Path | Description |
94
+ |--------|------|-------------|
95
+ | `POST` | `/memory` | Store a memory |
96
+ | `POST` | `/search` | Semantic search |
97
+ | `POST` | `/ingest` | Batch import |
98
+ | `GET` | `/memories` | List with pagination |
99
+ | `DELETE` | `/memory/{id}` | Delete a memory |
100
+ | `GET` | `/health` | Service health check |
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ make test # Unit tests (no services needed)
106
+ make test-integration # Integration tests (Qdrant + Ollama required)
107
+ make test-degraded # Degraded mode tests (Qdrant only)
108
+ make test-all # All tests
109
+ ```
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import httpx
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ app = typer.Typer(name="recall", help="Personal semantic memory CLI")
17
+ console = Console()
18
+
19
+ DEFAULT_API_URL = "http://127.0.0.1:8100"
20
+
21
+
22
+ def _get_api_url(api_url: Optional[str]) -> str:
23
+ return api_url or os.environ.get("RECALL_API_URL", DEFAULT_API_URL)
24
+
25
+
26
+ def _get_token(token: Optional[str]) -> Optional[str]:
27
+ return token or os.environ.get("RECALL_API_TOKEN") or None
28
+
29
+
30
+ def _build_headers(token: Optional[str]) -> dict:
31
+ h = {"Content-Type": "application/json"}
32
+ if token:
33
+ h["Authorization"] = f"Bearer {token}"
34
+ return h
35
+
36
+
37
+ def _handle_error(resp: httpx.Response, api_url: str) -> None:
38
+ if resp.status_code in (200, 201):
39
+ return
40
+ try:
41
+ data = resp.json()
42
+ detail = data.get("detail", resp.text)
43
+ except Exception:
44
+ detail = resp.text
45
+ console.print(f"[red]Error {resp.status_code}:[/red] {detail}")
46
+ raise typer.Exit(1)
47
+
48
+
49
+ def _connection_error(url: str) -> None:
50
+ console.print(
51
+ f"[red]Cannot reach recall service at {url}.[/red]\n"
52
+ f"Try: [bold]make serve[/bold] (from your recall project directory)"
53
+ )
54
+ raise typer.Exit(1)
55
+
56
+
57
+ def _is_tty() -> bool:
58
+ return sys.stdout.isatty()
59
+
60
+
61
+ @app.command()
62
+ def add(
63
+ text: str = typer.Argument(..., help="The memory text to store"),
64
+ tag: list[str] = typer.Option([], "--tag", "-t", help="Tag(s) to attach"),
65
+ source: str = typer.Option("cli", "--source", "-s", help="Source identifier"),
66
+ dedupe_key: Optional[str] = typer.Option(None, "--dedupe-key", "-d", help="Deduplication key"),
67
+ api_url: Optional[str] = typer.Option(None, "--api-url"),
68
+ token: Optional[str] = typer.Option(None, "--token"),
69
+ ):
70
+ """Store a memory."""
71
+ url = _get_api_url(api_url)
72
+ tok = _get_token(token)
73
+ payload = {"text": text, "tags": list(tag), "source": source, "dedupe_key": dedupe_key}
74
+
75
+ try:
76
+ resp = httpx.post(f"{url}/memory", json=payload, headers=_build_headers(tok), timeout=30)
77
+ except httpx.RequestError:
78
+ _connection_error(url)
79
+
80
+ _handle_error(resp, url)
81
+ data = resp.json()
82
+ console.print(f"[green]Stored[/green] id=[bold]{data['id']}[/bold] strategy={data['id_strategy']}")
83
+
84
+
85
+ @app.command()
86
+ def search(
87
+ query: str = typer.Argument(..., help="Search query"),
88
+ top_k: int = typer.Option(5, "--top-k", "-k"),
89
+ no_text: bool = typer.Option(False, "--no-text"),
90
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="table|json"),
91
+ api_url: Optional[str] = typer.Option(None, "--api-url"),
92
+ token: Optional[str] = typer.Option(None, "--token"),
93
+ ):
94
+ """Search memories by semantic similarity."""
95
+ url = _get_api_url(api_url)
96
+ tok = _get_token(token)
97
+ fmt = output or ("table" if _is_tty() else "json")
98
+ payload = {"query": query, "top_k": top_k, "include_text": not no_text}
99
+
100
+ try:
101
+ resp = httpx.post(f"{url}/search", json=payload, headers=_build_headers(tok), timeout=30)
102
+ except httpx.RequestError:
103
+ _connection_error(url)
104
+
105
+ _handle_error(resp, url)
106
+ results = resp.json().get("results", [])
107
+
108
+ if fmt == "json":
109
+ print(json.dumps(results, indent=2))
110
+ return
111
+
112
+ if not results:
113
+ console.print("[yellow]No results found.[/yellow]")
114
+ return
115
+
116
+ table = Table(title=f"Search: {query!r}")
117
+ table.add_column("Score", style="cyan", width=6)
118
+ table.add_column("ID", style="dim", width=36)
119
+ table.add_column("Tags")
120
+ table.add_column("Written At")
121
+ if not no_text:
122
+ table.add_column("Text")
123
+
124
+ for r in results:
125
+ row = [
126
+ f"{r['score']:.3f}",
127
+ r["id"],
128
+ ", ".join(r.get("tags", [])),
129
+ r.get("written_at", "")[:19],
130
+ ]
131
+ if not no_text:
132
+ row.append((r.get("text") or "")[:80])
133
+ table.add_row(*row)
134
+
135
+ console.print(table)
136
+
137
+
138
+ @app.command()
139
+ def ingest(
140
+ file: Path = typer.Argument(..., help="File to ingest"),
141
+ format: str = typer.Option("lines", "--format", "-f", help="lines|jsonl"),
142
+ source: str = typer.Option("ingest", "--source", "-s"),
143
+ auto_dedupe: bool = typer.Option(False, "--auto-dedupe"),
144
+ api_url: Optional[str] = typer.Option(None, "--api-url"),
145
+ token: Optional[str] = typer.Option(None, "--token"),
146
+ ):
147
+ """Ingest memories from a file."""
148
+ url = _get_api_url(api_url)
149
+ tok = _get_token(token)
150
+
151
+ if not file.exists():
152
+ console.print(f"[red]File not found:[/red] {file}")
153
+ raise typer.Exit(1)
154
+
155
+ raw = file.read_text(encoding="utf-8")
156
+ items: list[dict] = []
157
+
158
+ if format == "jsonl":
159
+ for line in raw.splitlines():
160
+ line = line.strip()
161
+ if not line:
162
+ continue
163
+ obj = json.loads(line)
164
+ items.append(obj)
165
+ else:
166
+ for line in raw.splitlines():
167
+ line = line.strip()
168
+ if not line:
169
+ continue
170
+ items.append({"text": line, "tags": [], "source": source})
171
+
172
+ if auto_dedupe:
173
+ for item in items:
174
+ src = item.get("source", source)
175
+ item["dedupe_key"] = hashlib.sha256(
176
+ (item["text"] + src).encode()
177
+ ).hexdigest()
178
+
179
+ batch_size = 100
180
+ total_succeeded = 0
181
+ total_failed = 0
182
+ all_errors: list[dict] = []
183
+
184
+ for i in range(0, len(items), batch_size):
185
+ batch = items[i : i + batch_size]
186
+ try:
187
+ resp = httpx.post(
188
+ f"{url}/ingest",
189
+ json={"items": batch},
190
+ headers=_build_headers(tok),
191
+ timeout=60,
192
+ )
193
+ except httpx.RequestError:
194
+ _connection_error(url)
195
+
196
+ _handle_error(resp, url)
197
+ data = resp.json()
198
+ total_succeeded += data.get("succeeded", 0)
199
+ total_failed += data.get("failed", 0)
200
+ for err in data.get("errors", []):
201
+ err["index"] += i
202
+ all_errors.append(err)
203
+
204
+ console.print(
205
+ f"[green]Ingested[/green] {total_succeeded} succeeded, "
206
+ f"[{'red' if total_failed else 'green'}]{total_failed}[/{'red' if total_failed else 'green'}] failed"
207
+ )
208
+ for err in all_errors:
209
+ console.print(f" [red]Error[/red] item {err['index']}: {err['error']}")
210
+
211
+
212
+ @app.command(name="list")
213
+ def list_memories(
214
+ limit: int = typer.Option(20, "--limit", "-l"),
215
+ cursor: Optional[str] = typer.Option(None, "--cursor"),
216
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="table|json"),
217
+ api_url: Optional[str] = typer.Option(None, "--api-url"),
218
+ token: Optional[str] = typer.Option(None, "--token"),
219
+ ):
220
+ """List stored memories."""
221
+ url = _get_api_url(api_url)
222
+ tok = _get_token(token)
223
+ fmt = output or ("table" if _is_tty() else "json")
224
+
225
+ params: dict = {"limit": limit}
226
+ if cursor:
227
+ params["cursor"] = cursor
228
+
229
+ try:
230
+ resp = httpx.get(f"{url}/memories", params=params, headers=_build_headers(tok), timeout=30)
231
+ except httpx.RequestError:
232
+ _connection_error(url)
233
+
234
+ _handle_error(resp, url)
235
+ data = resp.json()
236
+ memories = data.get("memories", [])
237
+ next_cursor = data.get("next_cursor")
238
+
239
+ if fmt == "json":
240
+ print(json.dumps(data, indent=2))
241
+ return
242
+
243
+ if not memories:
244
+ console.print("[yellow]No memories found.[/yellow]")
245
+ return
246
+
247
+ table = Table(title="Memories")
248
+ table.add_column("ID", style="dim", width=36)
249
+ table.add_column("Tags")
250
+ table.add_column("Source")
251
+ table.add_column("Written At")
252
+ table.add_column("Text")
253
+
254
+ for m in memories:
255
+ table.add_row(
256
+ m["id"],
257
+ ", ".join(m.get("tags", [])),
258
+ m.get("source", ""),
259
+ m.get("written_at", "")[:19],
260
+ (m.get("text") or "")[:60],
261
+ )
262
+
263
+ console.print(table)
264
+ if next_cursor:
265
+ console.print(f"\n[dim]Next cursor:[/dim] {next_cursor}")
266
+
267
+
268
+ @app.command()
269
+ def delete(
270
+ id: str = typer.Argument(..., help="Memory ID to delete"),
271
+ api_url: Optional[str] = typer.Option(None, "--api-url"),
272
+ token: Optional[str] = typer.Option(None, "--token"),
273
+ ):
274
+ """Delete a memory by ID."""
275
+ url = _get_api_url(api_url)
276
+ tok = _get_token(token)
277
+
278
+ try:
279
+ resp = httpx.delete(f"{url}/memory/{id}", headers=_build_headers(tok), timeout=30)
280
+ except httpx.RequestError:
281
+ _connection_error(url)
282
+
283
+ _handle_error(resp, url)
284
+ console.print(f"[green]Deleted[/green] {id}")
285
+
286
+
287
+ @app.command()
288
+ def status(
289
+ api_url: Optional[str] = typer.Option(None, "--api-url"),
290
+ token: Optional[str] = typer.Option(None, "--token"),
291
+ ):
292
+ """Show API health status."""
293
+ url = _get_api_url(api_url)
294
+
295
+ try:
296
+ resp = httpx.get(f"{url}/health", timeout=10)
297
+ except httpx.RequestError:
298
+ _connection_error(url)
299
+
300
+ data = resp.json()
301
+ color = "green" if data.get("status") == "ok" else "yellow" if data.get("status") == "degraded" else "red"
302
+ console.print(f"[{color}]Status:[/{color}] {data.get('status')}")
303
+ console.print(f" Qdrant: {data.get('qdrant')}")
304
+ console.print(f" Ollama: {data.get('ollama')}")
305
+
306
+
307
+ if __name__ == "__main__":
308
+ app()
@@ -0,0 +1,12 @@
1
+ services:
2
+ qdrant:
3
+ image: qdrant/qdrant:v1.13.2
4
+ ports:
5
+ - "127.0.0.1:6333:6333"
6
+ - "127.0.0.1:6334:6334"
7
+ volumes:
8
+ - qdrant_data:/qdrant/storage
9
+ restart: unless-stopped
10
+
11
+ volumes:
12
+ qdrant_data: