Przejdź do głównej zawartości

Wzorce i strategie NoSQL

Twój schemat MongoDB wyglądał czysto na demie: jeden gruby dokument user z osadzonymi zamówieniami, adresami i koszykiem. Sześć miesięcy później ta tablica ma 4000 wpisów, ocierasz się o limit 16 MB na dokument, a każdy odczyt ciągnie całość przez sieć. Agent, który wygenerował ten schemat, nigdy nie widział twoich wzorców dostępu ani stosunku odczytów do zapisów — więc zoptymalizował go pod demo, a nie pod produkcję.

W NoSQL nie ma migracji schematu, za którymi można się schować. Model danych jest projektem, a zła decyzja „osadzić czy referencjonować” jest kosztowna do odkręcenia, gdy masz już ruch. Dobra wiadomość: agenci kodujący AI radzą sobie z tym świetnie, kiedy z góry dasz im ograniczenia i pozwolisz weryfikować na żywej bazie przez serwer MCP.

  • Prompt decyzyjny, który zmusza agenta, by uzasadnił osadzenie vs. referencję na podstawie twoich faktycznych wzorców dostępu i stosunku odczytów do zapisów
  • Prompt do potoku agregacji, który wymaga planu explain() i indeksu pokrywającego, a nie tylko zapytania, które „działa”
  • Prompt do projektu jednej tabeli DynamoDB, który wymienia dokładne wzorce dostępu i prosi o projekcje GSI
  • Stan „przed/po” podłączeniu serwerów MCP MongoDB i Neo4j, by agent introspektował twój prawdziwy schemat zamiast zgadywać
  • Gotowe do uruchomienia fragmenty AWS SDK v3 i Neo4j 5, które dziś wkleisz do realnego projektu

Wzorzec jest ten sam dla każdego magazynu: daj agentowi wzorce dostępu i ograniczenie, każ mu wytworzyć projekt jako artefakt, a potem niech zweryfikuje go na żywej instancji przez MCP. Bez MCP agent halucynuje nazwy kolekcji i kształty indeksów; z MCP agent czyta twój prawdziwy schemat i dowodzi swoich zapytań.

To największe pojedyncze usprawnienie pracy z NoSQL przy udziale agenta. Serwer MCP MongoDB (mongodb-mcp-server, na npm) pozwala agentowi listować kolekcje, próbkować dokumenty, uruchamiać explain() i tworzyć indeksy na twoim prawdziwym klastrze. Serwer MCP Neo4j Cypher (mcp-neo4j-cypher, w Pythonie — uruchamiany przez uvx) robi to samo dla grafu. Konfiguracja MCP jest identyczna we wszystkich trzech narzędziach poza tym, gdzie żyje konfiguracja.

Dodaj do .cursor/mcp.json (projekt) lub ~/.cursor/mcp.json (globalnie), a następnie włącz serwer w Settings → MCP:

{
"mcpServers": {
"mongodb": {
"command": "npx",
"args": ["-y", "mongodb-mcp-server", "--connectionString", "mongodb://localhost:27017/shop"]
}
}
}

Krok 2 — Uczyń decyzję „osadzić vs. referencjonować” jawną

Dział zatytułowany „Krok 2 — Uczyń decyzję „osadzić vs. referencjonować” jawną”

Najbardziej brzemienną w skutki decyzją w magazynie dokumentów jest to, czy powiązane dane są osadzone, czy referencjonowane. Agent trafia z tym za pierwszym razem, gdy wręczysz mu wzorce dostępu i stosunek odczytów do zapisów, zamiast prosić go o „zaprojektowanie dobrego schematu”.

Dobra odpowiedź osadza pozycje zamówienia (ograniczone, niezmienne, zawsze czytane razem), przechowuje zdenormalizowaną migawkę customer: { id, name } zamiast robić $lookup przy każdym odczycie i trzyma osobną kolekcję orderItems tylko wtedy, gdy pozycje mogą rosnąć bez ograniczeń. Wytworzony projekt to artefakt, który recenzujesz — agent powinien wytworzyć go jako opisany schemat, a nie zakopać w prozie.

Krok 3 — Generuj potoki agregacji, które same się dowodzą

Dział zatytułowany „Krok 3 — Generuj potoki agregacji, które same się dowodzą”

Potoki agregacji to miejsce, w którym AI oszczędza najwięcej czasu, ale też najczęściej tworzy zapytanie zwracające poprawne wyniki przy pełnym skanie kolekcji. Lekarstwem jest wymaganie planu wykonania jako części dostarczanego rezultatu.

Z podłączonym serwerem MCP MongoDB agent może faktycznie uruchomić to explain() i iterować, aż etap trafi w IXSCAN — domykając pętlę, zamiast zostawiać tobie odkrycie pełnego skanu na produkcji.

Ta sama pętla obejmuje pracę w czasie rzeczywistym. Strumień zmian MongoDB to kilka linijek i jest identyczny we wszystkich trzech narzędziach — wartością jest prompt, a nie szablonowy kod:

import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const products = client.db('shop').collection('products');
// Tokeny wznowienia przetrwają restart; na produkcji przechowuj ostatni trwale.
const changeStream = products.watch(
[{ $match: { 'fullDocument.inventory.available': { $lt: 10 } } }],
{ fullDocument: 'updateLookup' }
);
for await (const change of changeStream) {
await notifyLowStock(change.fullDocument);
}

DynamoDB najsurowiej karze za niejasne prompty: pomyl klucz partycji, a stworzysz gorące partycje, które dławią się pod obciążeniem. Wymień z góry każdy wzorzec dostępu i każ agentowi zmapować każdy z nich na warunek klucza.

Powstałą mapę kluczy i tabelę GSI traktuj jako artefakt projektowy. Część wykonywalna jest niewielka — i musi używać AWS SDK v3, bo v2 jest po zakończeniu wsparcia. Oto prawdziwe, gotowe do uruchomienia zapytanie pod wzorzec dostępu getUserTasks:

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb';
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
// GSI1: PK = USER#<id>, SK = TASK#<dueDate>#<taskId>
export async function getUserTasks(userId, fromDate) {
const { Items } = await ddb.send(
new QueryCommand({
TableName: 'SaaSPlatform',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk AND GSI1SK >= :start',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
':start': `TASK#${fromDate}`,
},
ScanIndexForward: true, // rosnąco według terminu
})
);
return Items;
}

Zwróć uwagę na await ddb.send(new QueryCommand(...)) — łańcuch .query(params).promise() z v2 już nie obowiązuje. Zapisy wsadowe mają ten sam kształt z BatchWriteCommand (wciąż ograniczone do 25 pozycji na żądanie).

Dla danych silnie powiązanych (grafy społecznościowe, rekomendacje, siatki oszustw) graf bije złączenia. Poproś agenta najpierw o model węzłów/relacji, a potem o Cypher — i upewnij się, że emituje składnię Neo4j 5, bo dane treningowe agenta są pełne usuniętej formy ograniczeń z v4.

Poprawne ograniczenia Neo4j 5 i zapytanie rekomendacyjne wyglądają tak:

CREATE CONSTRAINT user_id IF NOT EXISTS
FOR (u:User) REQUIRE u.id IS UNIQUE;
CREATE CONSTRAINT post_id IF NOT EXISTS
FOR (p:Post) REQUIRE p.id IS UNIQUE;
// Posty, które znajomi polubili w ciągu ostatnich 7 dni, a których ten użytkownik nie obejrzał
MATCH (user:User {id: $userId})-[:FRIENDS]-(friend:User)
MATCH (friend)-[:LIKED]->(post:Post)
WHERE NOT (user)-[:VIEWED]->(post)
AND post.createdAt > datetime() - duration('P7D')
WITH post, count(DISTINCT friend) AS friendLikes
RETURN post.id, post.content, friendLikes
ORDER BY friendLikes DESC
LIMIT 10;

Z podłączonym serwerem MCP Neo4j agent uruchamia każde zapytanie przed oddaniem go, wyłapując literówki w etykietach i naleciałości z v4, które inaczej wysadziłyby się w czasie działania.

Przechowywanie wektorów osadzeń obok dokumentów pozwala robić wyszukiwanie semantyczne bez osobnej bazy wektorowej. W MongoDB Atlas nowoczesnym API jest typ indeksu vectorSearch z tablicą fields i numDimensions — starsze mapowanie knnVector jest przestarzałe i niekompatybilne z etapem agregacji $vectorSearch, którym faktycznie odpytujesz.

from pymongo.operations import SearchIndexModel
# Indeks vectorSearch wymaga SearchIndexModel — forma create_search_index()
# z czystym słownikiem tworzy tylko indeksy Atlas Search i ignoruje `type`.
search_index_model = SearchIndexModel(
definition={
"fields": [
{
"type": "vector",
"path": "text_embedding",
"numDimensions": 384, # musi dokładnie odpowiadać twojemu modelowi osadzeń
"similarity": "cosine",
},
{"type": "filter", "path": "price"},
{"type": "filter", "path": "categories"},
]
},
name="product_embeddings",
type="vectorSearch",
)
db.products.create_search_index(model=search_index_model)
# Odpytuj go etapem $vectorSearch, wstępnie filtrując po zaindeksowanych polach filtra
pipeline = [
{
"$vectorSearch": {
"index": "product_embeddings",
"path": "text_embedding",
"queryVector": query_embedding, # 384-wymiarowa lista z twojego modelu
"numCandidates": 200,
"limit": 10,
"filter": {"price": {"$lte": 100}},
}
},
{"$addFields": {"score": {"$meta": "vectorSearchScore"}}},
{"$project": {"text_embedding": 0}},
]
  • Niekontrolowany wzrost tablicy / limit 16 MB. Osadzanie events, comments czy lineItems, które rosną w nieskończoność, w końcu uderza w limit 16 MB na dokument w MongoDB i spowalnia każdy odczyt. Referencjonuj do kolekcji potomnej, gdy tylko tablica może rosnąć bez ograniczeń; poproś agenta, by oznaczył to na etapie projektowania schematu.
  • Gorące partycje DynamoDB. Klucz partycji o niskiej kardynalności (np. TENANT#<id> dla kilku ogromnych najemców) dławi się pod obciążeniem niezależnie od zaaprowizowanej pojemności. Dodaj sufiks shardujący zapisy (TENANT#<id>#<shard>) i rób scatter-gather przy odczycie.
  • Superwęzły Neo4j. Celebryta z milionami krawędzi FRIENDS zamienia przejścia po grafie w skany tabeli. Ogranicz rozejście (fan-out) w zapytaniach lub zamodeluj gorącą relację jako osobny węzeł.
  • Nieaktualne wektory osadzeń. Wyniki wektorowe po cichu degradują się, gdy zmieniasz model osadzeń, ale nie reindeksujesz istniejących dokumentów, lub gdy numDimensions przestaje pasować do modelu. Przeliczaj osadzenia przy zmianie modelu i asercjuj liczbę wymiarów.
  • Agent wymyśla schemat. Bez połączenia MCP zgaduje nazwy kolekcji i pól. Jeśli zapytania odwołują się do pól, których nie masz, zatrzymaj się i podłącz serwer MCP MongoDB lub Neo4j, by czytał prawdziwy schemat.
  • Wzorce i strategie SQL — kiedy model relacyjny jest właściwym wyborem i jak o niego promptować
  • Wzorce ORM — bezpieczny typowo dostęp do danych nad tymi magazynami z Prismą, Drizzle i Mongoose
  • Wzorce migracji — przenoszenie między magazynami i bezpieczna ewolucja modeli NoSQL