{
  "source_file": "0930",
  "source_platform": "Substack",
  "author": "Simon Willison's Weblog",
  "title_listed": "LLM Now Provides Tools for Working with Embeddings",
  "keywords": "数据探索,SQLite,LLM工具,数据分析",
  "domain": "数据处理",
  "content_type": "工具介绍",
  "is_paid_flag": "否",
  "url": "https://simonwillison.net/2023/Sep/4/llm-embeddings/",
  "publish_date": "2023-09",
  "corpus_stage": "SFT",
  "notes": "Datasette+LLM组合用于数据探索分析",
  "id": "4f1cac52c2dc",
  "collected_at": "2026-10-01T19:50:46",
  "title": "LLM now provides tools for working with embeddings",
  "body_markdown": "Simon Willison’s Weblog\nNewsletter\nSponsored by:\nPomerium — Static OAuth scopes won't stop AI agent breakouts.\nUpgrade to Pomerium's agentic access management trusted by NVIDIA and OpenAI\nLLM now provides tools for working with embeddings\n4th September 2023\nLLM\nis my Python library and command-line tool for working with language models. I just released\nLLM 0.9\nwith a new set of features that extend LLM to provide tools for working with\nembeddings\n.\nThis is a long post with a lot of theory and background. If you already know what embeddings are, here’s a TLDR you can try out straight away:\n#\nInstall LLM\npip install llm\n#\nIf you already installed via Homebrew/pipx you can upgrade like this:\nllm install -U llm\n#\nInstall the llm-sentence-transformers plugin\nllm install llm-sentence-transformers\n#\nInstall the all-MiniLM-L6-v2 embedding model\nllm sentence-transformers register all-MiniLM-L6-v2\n#\nGenerate and store embeddings for every README.md in your home directory, recursively\nllm embed-multi readmes \\\n  --model sentence-transformers/all-MiniLM-L6-v2 \\\n  --files\n~\n/\n'\n**/README.md\n'\n#\nAdd --store to store the text content as well\n#\nRun a similarity search for \"sqlite\" against those embeddings\nllm similar readmes -c sqlite\nFor everyone else, read on and the above example should hopefully all make sense.\nEmbeddings\nLLM’s new embedding features\nEmbedding collections\nEmbedding similarity search\nEmbedding in bulk\nEmbedding files in a directory\nEmbeddings in Python\nClustering with llm-cluster\nFuture plans\nIndexing\nChunking\nGet involved\nEmbeddings\nEmbeddings are a fascinating concept within the larger world of language models.\nI explain embeddings in detail (with both a video and heavily annotated slides) in\nEmbeddings: What they are and why they matter\n.\nAn embedding model lets you take a string of text—a word, sentence, paragraph or even a whole document—and turn that into an array of floating point numbers called an\nembedding vector\n.\nA model will always produce the same length of array—1,536 numbers for the\nOpenAI embedding model\n, 384 for\nall-MiniLM-L6-v2\n—but the array itself is inscrutable. What are you meant to do with it?\nThe answer is that you can compare them. I like to think of an embedding vector as a location in 1,536-dimensional space. The distance between two vectors is a measure of how semantically similar they are in meaning, at least according to the model that produced them.\n“One happy dog” and “A playful hound” will end up close together, even though they don’t share any keywords. The embedding vector represents the language model’s interpretation of the meaning of the text.\nThings you can do with embeddings include:\nFind\nrelated items\n. I use this on\nmy TIL site\nto display related articles, as described in\nStoring and serving related documents with openai-to-sqlite and embeddings\n.\nBuild\nsemantic search\n. As shown above, an embeddings-based search engine can find content relevant to the user’s search term even if none of the keywords match.\nImplement\nretrieval augmented generation\n—the trick where you take a user’s question, find relevant documentation in your own corpus and use that to get an LLM to spit out an answer. More on that\nhere\n.\nClustering\n: you can find clusters of nearby items and identify patterns in a corpus of documents.\nClassification\n: calculate the embedding of a piece of text and compare it to pre-calculated “average” embeddings for different categories.\nLLM’s new embedding features\nMy goal with LLM is to provide a plugin-driven abstraction around a growing collection of language models. I want to make installing, using and comparing these models as easy as possible.\nThe new release adds several command-line tools for working with embeddings, plus a new Python API for working with embeddings in your own code.\nIt also adds support for installing additional embedding models via plugins. I’ve released one plugin for this so far:\nllm-sentence-transformers\n, which adds support for new models based on the\nsentence-transformers\nlibrary.\nThe example above shows how to use\nsentence-transformers\n. LLM also supports API-driven access to the OpenAI\nada-002\nmodel.\nHere’s how to embed some text using\nada-002\n, assuming you have\ninstalled LLM already\n:\n#\nSet your OpenAI API key\nllm keys\nset\nopenai\n#\n<paste key here>\n#\nEmbed some text\nllm embed -m ada-002 -c\n\"\nHello world\n\"\nThis will output a huge JSON list of floating point numbers to your terminal. You can add\n-f base64\n(or\n-f hex\n) to get that back in a different format, though none of these outputs are instantly useful.\nEmbeddings are much more interesting when you store them.\nLLM already uses SQLite to\nstore prompts and responses\n. It was a natural fit to use SQLite to store embeddings as well.\nEmbedding collections\nLLM 0.9 introduces the concept of a\ncollection\nof embeddings. A collection has a name—like\nreadmes\n—and contains a set of embeddings, each of which has an ID and an embedding vector.\nAll of the embeddings in a collection are generated by the same model, to ensure they can be compared with each others.\nThe\nllm embed\ncommand can store the vector in the database instead of returning it to the console. Pass it the name of an existing (or to-be-created) collection and the ID to use to store the embedding.\nHere we’ll store the embedding for the phrase “Hello world” in a collection called\nphrases\nwith the ID\nhello\n, using that\nada-002\nembedding model:\nllm embed phrases hello -m ada-002 -c\n\"\nHello world\n\"\nFuture phrases can be added without needing to specify the model again, since it is remembered by the collection:\nllm embed phrases goodbye -c\n\"\nGoodbye world\n\"\nThe\nllm embed-db collections\nshows a list of collections:\nphrases: ada-002\n  2 embeddings\nreadmes: sentence-transformers/all-MiniLM-L6-v2\n  16796 embeddings\nThe data is stored in a SQLite\nembeddings\ntable with the following schema:\nCREATE TABLE [collections] (\n   [id]\nINTEGER\nPRIMARY KEY\n,\n   [name]\nTEXT\n,\n   [model]\nTEXT\n);\nCREATE\nTABLE\n\"\nembeddings\n\" (\n   [collection_id]\nINTEGER\nREFERENCES\n[collections]([id]),\n   [id]\nTEXT\n,\n   [embedding] BLOB,\n   [content]\nTEXT\n,\n   [content_hash] BLOB,\n   [metadata]\nTEXT\n,\n   [updated]\nINTEGER\n,\nPRIMARY KEY\n([collection_id], [id])\n);\n\nCREATE UNIQUE INDEX [idx_collections_name]\nON\n[collections] ([name]);\nCREATE INDEX [idx_embeddings_content_hash]\nON\n[embeddings] ([content_hash]);\nBy default this is the SQLite database at the location revealed by\nllm embed-db path\n, but you can pass\n--database my-embeddings.db\nto various LLM commands to use a different database.\nEach embedding vector is stored as a binary BLOB in the\nembedding\ncolumn, consisting of those floating point numbers packed together as 32 bit floats.\nThe\ncontent_hash\ncolumn contains a MD5 hash of the content. This helps avoid re-calculating the embedding (which can cost actual money for API-based embedding models like\nada-002\n) unless the content has changed.\nThe\ncontent\ncolumn is usually\nnull\n, but can contain a copy of the original text content if you pass the\n--store\noption to the\nllm embed\ncommand.\nmetadata\ncan contain a JSON object with metadata, if you pass\n--metadata '{\"json\": \"goes here\"}\n.\nYou don’t have to pass content using\n-c\n—you can instead pass a file path using the\n-i/--input\noption:\nllm embed docs llm-setup -m ada-002 -i llm/docs/setup.md\nOr pipe things to standard input like this:\ncat llm/docs/setup.md\n|\nllm embed docs llm-setup -m ada-002 -i -\nEmbedding similarity search\nOnce you’ve built a collection, you can search for similar embeddings using the\nllm similar\ncommand.\nThe\n-c \"term\"\noption will embed the text you pass in using the embedding model for the collection and use that as the comparison vector:\nllm similar readmes -c sqlite\nYou can also pass the ID of an object in that collection to use that embedding instead. This gets you related documents, for example:\nllm similar readmes sqlite-utils/README.md\nThe output from this command is currently newline-delimited JSON.\nEmbedding in bulk\nThe\nllm embed\ncommand embeds a single string at a time.\nllm embed-multi\nis much more powerful: you can feed a CSV or JSON file, a SQLite database or even have it read from a directory of files in order to embed multiple items at once.\nMany embeddings models are optimized for batch operations, so embedding multiple items at a time can provide a significant speed boost.\nThe\nembed-multi\ncommand is described\nin detail in the documentation\n. Here are a couple of fun things you can do with it.\nFirst, I’m going to create embeddings for every single one of my Apple Notes.\nMy\napple-notes-to-sqlite\ntool can export Apple Notes to a SQLite database. I’ll run that first:\napple-notes-to-sqlite notes.db\nThis took quite a while to run on my machine and generated a 828M SQLite database containing 6,462 records!\nNext, I’m going to embed the content of all of those notes using the\nsentence-transformers/all-MiniLM-L6-v2\nmodel:\nllm embed-multi notes \\\n  -d notes.db \\\n  --sql\n'\nselect id, title, body from notes\n'\n\\\n  -m sentence-transformers/all-MiniLM-L6-v2\nThis took around 15 minutes to run, and increased the size of my database by 13MB.\nThe\n--sql\noption here specifies a SQL query. The first column must be an\nid\n, then any subsequent columns will be concatenated together to form the content to embed.\nIn this case the embeddings are written back to the same\nnotes.db\ndatabase that the content came from.\nAnd now I can run embedding similarity operations against all of my Apple notes!\nllm similar notes -d notes.db -c\n'\nideas for blog posts\n'\nEmbedding files in a directory\nLet’s revisit the example from the top of this post. In this case, I’m using the\n--files\noption to search for files on disk and embed each of them:\nllm embed-multi readmes \\\n  --model sentence-transformers/all-MiniLM-L6-v2 \\\n  --files\n~\n/\n'\n**/README.md\n'\nThe\n--files\noption takes two arguments: a path to a directory and a pattern to match against filenames. In this case I’m searching my home directory recursively for any files named\nREADME.md\n.\nRunning this command gives me embeddings for all of my README.md files, which I can then search against like this:\nllm similar readmes -c sqlite\nEmbeddings in Python\nSo far I’ve only covered the command-line tools. LLM 0.9 also introduces a new Python API for working with embeddings.\nThere are two aspects to this. If you just want to embed content and handle the resulting vectors yourself, you can use\nllm.get_embedding_model()\n:\nimport\nllm\n# This takes model IDs and aliases defined by plugins:\nmodel\n=\nllm\n.\nget_embedding_model\n(\n\"sentence-transformers/all-MiniLM-L6-v2\"\n)\nvector\n=\nmodel\n.\nembed\n(\n\"This is text to embed\"\n)\nvector\nwill then be a Python list of floating point numbers.\nYou can serialize that to the same binary format that LLM uses like this:\nbinary_vector\n=\nllm\n.\nencode\n(\nvector\n)\n# And to deserialize:\nvector\n=\nllm\n.\ndecode\n(\nbinary_vector\n)\nThe second aspect of the Python API is the\nllm.Collection\nclass, for working with collections of embeddings. This example code is quoted\nfrom the documentation\n:\nimport\nsqlite_utils\nimport\nllm\n# This collection will use an in-memory database that will be\n# discarded when the Python process exits\ncollection\n=\nllm\n.\nCollection\n(\n\"entries\"\n,\nmodel_id\n=\n\"ada-002\"\n)\n# Or you can persist the database to disk like this:\ndb\n=\nsqlite_utils\n.\nDatabase\n(\n\"my-embeddings.db\"\n)\ncollection\n=\nllm\n.\nCollection\n(\n\"entries\"\n,\ndb\n,\nmodel_id\n=\n\"ada-002\"\n)\n# You can pass a model directly using model= instead of model_id=\nembedding_model\n=\nllm\n.\nget_embedding_model\n(\n\"ada-002\"\n)\ncollection\n=\nllm\n.\nCollection\n(\n\"entries\"\n,\ndb\n,\nmodel\n=\nembedding_model\n)\n# Store a string in the collection with an ID:\ncollection\n.\nembed\n(\n\"hound\"\n,\n\"my happy hound\"\n)\n# Or to store content and extra metadata:\ncollection\n.\nembed\n(\n\"hound\"\n,\n\"my happy hound\"\n,\nmetadata\n=\n{\n\"name\"\n:\n\"Hound\"\n},\nstore\n=\nTrue\n)\n# Or embed things in bulk:\ncollection\n.\nembed_multi\n(\n    [\n        (\n\"hound\"\n,\n\"my happy hound\"\n),\n        (\n\"cat\"\n,\n\"my dissatisfied cat\"\n),\n    ],\n# Add this to store the strings in the content column:\nstore\n=\nTrue\n,\n)\nAs with everything else in LLM, the goal is that anything you can do with the CLI can be done with the Python API, and vice-versa.\nClustering with llm-cluster\nAnother interesting application of embeddings is that you can use them to cluster content—identifying patterns in a corpus of documents.\nI’ve started exploring this area with a new plugin, called\nllm-cluster\n.\nYou can install it like this:\nllm install llm-cluster\nLet’s create a new collection using data pulled from GitHub. I’m going to import all of the\nLLM issues\nfrom the GitHub API, using my\npaginate-json\ntool:\npaginate-json\n'\nhttps://api.github.com/repos/simonw/llm/issues?state=all&filter=all\n'\n\\\n|\njq\n'\n[.[] | {id: .id, title: .title}]\n'\n\\\n|\nllm embed-multi llm-issues - \\\n    --database issues.db \\\n    --model sentence-transformers/all-MiniLM-L6-v2 \\\n    --store\nRunning this gives me a\nissues.db\nSQLite database with 218 embeddings contained in a collection called\nllm-issues\n.\nNow let’s try out the\nllm-cluster\ncommand, requesting ten clusters from that collection:\nllm cluster llm-issues --database issues.db 10\nThe output from this command, truncated, looks like this:\n[\n  {\n\"id\"\n:\n\"\n0\n\"\n,\n\"items\"\n: [\n      {\n\"id\"\n:\n\"\n1784149135\n\"\n,\n\"content\"\n:\n\"\nTests fail with pydantic 2\n\"\n},\n      {\n\"id\"\n:\n\"\n1837084995\n\"\n,\n\"content\"\n:\n\"\nAllow for use of Pydantic v1 as well as v2.\n\"\n},\n      {\n\"id\"\n:\n\"\n1857942721\n\"\n,\n\"content\"\n:\n\"\nGet tests passing against Pydantic 1\n\"\n}\n    ]\n  },\n  {\n\"id\"\n:\n\"\n1\n\"\n,\n\"items\"\n: [\n      {\n\"id\"\n:\n\"\n1724577618\n\"\n,\n\"content\"\n:\n\"\nBetter ways of storing and accessing API keys\n\"\n},\n      {\n\"id\"\n:\n\"\n1772024726\n\"\n,\n\"content\"\n:\n\"\nSupport for `-o key value` options such as `temperature`\n\"\n},\n      {\n\"id\"\n:\n\"\n1784111239\n\"\n,\n\"content\"\n:\n\"\n`--key` should be used in place of the environment variable\n\"\n}\n    ]\n  },\n  {\n\"id\"\n:\n\"\n8\n\"\n,\n\"items\"\n: [\n      {\n\"id\"\n:\n\"\n1835739724\n\"\n,\n\"content\"\n:\n\"\nBump the python-packages group with 1 update\n\"\n},\n      {\n\"id\"\n:\n\"\n1848143453\n\"\n,\n\"content\"\n:\n\"\nPython library support for adding aliases\n\"\n},\n      {\n\"id\"\n:\n\"\n1857268563\n\"\n,\n\"content\"\n:\n\"\nBump the python-packages group with 1 update\n\"\n}\n    ]\n  }\n]\nThese look pretty good! But wouldn’t it be neat if we had a snappy title for each one?\nThe\n--summary\noption can provide exactly that, by piping the members of each cluster through a call to another LLM in order to generate a useful summary.\nllm cluster llm-issues --database issues.db 10 --summary\nThis uses\ngpt-3.5-turbo\nto generate a summary for each cluster, with this default prompt:\nShort, concise title for this cluster of related documents.\nThe results I got back are pretty good, including:\nTemplate Storage and Management Improvements\nPackage and Dependency Updates and Improvements\nAdding Conversation Mechanism and Tools\nI tried the same thing using a Llama 2 model\nrunning on my own laptop\n, with a custom prompt:\nllm cluster llm-issues --database issues.db 10 \\\n  --summary --model mlc-chat-Llama-2-13b-chat-hf-q4f16_1 \\\n  --prompt 'Concise title for this cluster of related documents, just return the title'\nI didn’t quite get what I wanted! Llama 2 is proving a lot harder to prompt, so each cluster came back with something that looked like this:\nSure! Here’s a concise title for this cluster of related documents:\n“Design Improvements for the Neat Prompt System”\nThis title captures the main theme of the documents, which is to improve the design of the Neat prompt system. It also highlights the focus on improving the system’s functionality and usability\nllm-cluster\nonly took a few hours to throw together, which I’m seeing as a positive indicator that the LLM library is developing in the right direction.\nFuture plans\nThe two future features I’m most excited about are indexing and chunking.\nIndexing\nThe\nllm similar\ncommand and\ncollection.similar()\nPython method currently use effectively the slowest brute force approach possible: calculate a cosine difference between input vector and every other embedding in the collection, then sort the results.\nThis works fine for collections with a few hundred items, but will start to suffer for collections of 100,000 or more.\nThere are plenty of potential ways of speeding this up: you can run a vector index like\nFAISS\nor\nhnswlib\n, use a database extension like\nsqlite-vss\nor\npgvector\n, or turn to a hosted vector database like\nPinecone\nor\nMilvus\n.\nWith this many potential solutions, the obvious answer for LLM is to address this with plugins.\nI’m still thinking through the details, but the core idea is that users should be able to define an index against one or more collections, and LLM will then coordinate updates to that index. These may not happen in real-time—some indexes can be expensive to rebuild, so there are benefits to applying updates in batches.\nI experimented with FAISS earlier this year in\ndatasette-faiss\n. That’s likely to be the base for my first implementation.\nThe\nembeddings table\nhas an\nupdated\ntimestamp column to support this use-case—so indexers can run against just the items that have changed since the last indexing run.\nFollow\nissue #216\nfor updates on this feature.\nChunking\nWhen building an embeddings-based search engine, the hardest challenge is deciding how best to “chunk” the documents.\nUsers will type in short phrases or questions. The embedding for a four word question might not necessarily map closely to the embedding of a thousand word article, even if the article itself should be a good match for that query.\nTo maximize the chance of returning the most relevant content, we need to be smarter about what we embed.\nI’m still trying to get a good feeling for the strategies that make sense here. Some that I’ve seen include:\nSplit a document up into fixed length shorter segments.\nSplit into segments but including a ~10% overlap with the previous and next segments, to reduce problems caused by words and sentences being split in a way that disrupts their semantic meaning.\nSplitting by sentence, using NLP techniques.\nSplitting into higher level sections, based on things like document headings.\nThen there are more exciting, LLM-driven approaches:\nGenerate an LLM summary of a document and embed that.\nAsk an LLM “What questions are answered by the following text?” and then embed each of the resulting questions!\nIt’s possible to try out these different techniques using LLM already: write code that does the splitting, then feed the results to\nCollection.embed_multi()\nor\nllm embed-multi\n.\nBut... it would be really cool if LLM could split documents for you—with the splitting techniques themselves defined by plugins, to make it easy to try out new approaches.\nGet involved\nIt should be clear by now that the potential scope of the LLM project is enormous. I’m trying to use plugins to tie together an enormous and rapidly growing ecosystem of models and techniques into something that’s as easy for people to work with and build on as possible.\nThere are plenty of ways you can help!\nJoin the #llm Discord\nto talk about the project.\nTry out plugins and run different models with them. There are\n12 plugins already\n, and several of those can be used to run dozens if not hundreds of models (\nllm-mlc\n,\nllm-gpt4all\nand\nllm-llama-cpp\nin particular). I’ve hardly scratched the surface of these myself, and I’m testing exclusively on Apple Silicon. I’m really keen to learn more about which models work well, which models don’t and which perform the best on different hardware.\nTry\nbuilding a plugin\nfor a new model. My dream here is that every significant Large Language Model will have an LLM plugin that makes it easy to install and use.\nBuild stuff using LLM and let me know what you’ve built. Nothing fuels an open source project more than stories of cool things people have built with it.\nPosted\n4th September 2023\nat 8:32 pm · Follow me on\nMastodon\n,\nBluesky\n,\nTwitter\nor\nsubscribe to my newsletter\nMore recent articles\nHello Python 3.15, goodbye 3.10, and now I can use 3.11\n- 12th October 2026\nA new feature for my blog, built using my voice\n- 9th October 2026\nClaude Haiku 5.5\n- 7th October 2026\nWe're going to need default hard budget caps on pretty much everything\n- 3rd October 2026\nThis is\nLLM now provides tools for working with embeddings\nby Simon Willison, posted on\n4th September 2023\n.\nPart of series\nNew releases of LLM\nllm, ttok and strip-tags - CLI tools for working with ChatGPT and other LLMs\n- May 18, 2023, 9:04 p.m.\nMy LLM CLI tool now supports self-hosted language models via plugins\n- July 12, 2023, 2:24 p.m.\nLLM now provides tools for working with embeddings\n- Sept. 4, 2023, 8:32 p.m.\nLLM 0.13: The annotated release notes\n- Jan. 26, 2024, 11:08 p.m.\nYou can now run prompts against images, audio and video in your terminal using LLM\n- Oct. 29, 2024, 3:09 p.m.\nLLM 0.22, the annotated release notes\n- Feb. 17, 2025, 6:19 a.m.\nStructured data extraction from unstructured content using LLM schemas\n- Feb. 28, 2025, 5:07 p.m.\n… more\ncli\n109\nopen-source\n322\nprojects\n558\nsqlite\n488\nai\n2,279\ngenerative-ai\n2,021\nvector-search\n21\nllms\n1,987\nembeddings\n62\nllm\n639\nrag\n35\nNext:\nBuild an image search engine with llm-clip, chat with models with llm chat\nPrevious:\nDatasette 1.0a4 and 1.0a5, plus weeknotes\nMonthly briefing\nSponsor me for\n$10/month\nand get a curated email digest of the month's most important LLM developments.\nPay me to send you less!\nSponsor & subscribe\nDisclosures\nColophon\n©\n2002\n2003\n2004\n2005\n2006\n2007\n2008\n2009\n2010\n2011\n2012\n2013\n2014\n2015\n2016\n2017\n2018\n2019\n2020\n2021\n2022\n2023\n2024\n2025\n2026",
  "metadata": {
    "title": "LLM now provides tools for working with embeddings",
    "description": null,
    "keywords": null,
    "author": "Simon Willison",
    "og:site_name": "Simon Willison’s Weblog",
    "og:url": "https://simonwillison.net/2023/Sep/4/llm-embeddings/",
    "og:title": "LLM now provides tools for working with embeddings",
    "og:image": "https://static.simonwillison.net/static/2023/wordcamp-llms/llm-work-for-you.055.jpeg",
    "og:type": "article",
    "og:description": "LLM is my Python library and command-line tool for working with language models. I just released LLM 0.9 with a new set of features that extend LLM to provide tools …",
    "og:updated_time": "1693859559",
    "twitter:card": "summary",
    "twitter:image": "https://static.simonwillison.net/static/2023/wordcamp-llms/llm-work-for-you.055.jpeg",
    "twitter:creator": "@simonw"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/4f1cac52c2dc/code/body-000.txt",
      "chars": 646,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-001.txt",
      "chars": 121,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-002.txt",
      "chars": 51,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-003.txt",
      "chars": 44,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-004.txt",
      "chars": 98,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-005.txt",
      "chars": 497,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-006.txt",
      "chars": 56,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-007.txt",
      "chars": 64,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-008.txt",
      "chars": 29,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-009.txt",
      "chars": 42,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-010.txt",
      "chars": 30,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-011.txt",
      "chars": 129,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-012.txt",
      "chars": 55,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-013.txt",
      "chars": 104,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-014.txt",
      "chars": 29,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-015.txt",
      "chars": 186,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-016.txt",
      "chars": 91,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-017.txt",
      "chars": 1000,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-018.txt",
      "chars": 23,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-019.txt",
      "chars": 256,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-020.txt",
      "chars": 46,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-021.txt",
      "chars": 1131,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-022.txt",
      "chars": 56,
      "language": "text"
    },
    {
      "file": "attachments/4f1cac52c2dc/code/body-023.txt",
      "chars": 196,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://static.simonwillison.net/static/2023/wordcamp-llms/llm-work-for-you.055.jpeg",
      "bytes": 312294,
      "reused": true,
      "local_path": "attachments/4f1cac52c2dc/images/000.jpeg",
      "src": "https://static.simonwillison.net/static/2023/wordcamp-llms/llm-work-for-you.055.jpeg",
      "alt": "On the left is a text post from one of my sites: Storing and serving related documents with openai-to-sqlite and embeddings. An arrow points to a huge JSON array on the right, with the label 1536 floating point numbers.",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://static.simonwillison.net/static/2023/wordcamp-llms/llm-work-for-you.056.jpeg",
      "bytes": 162315,
      "reused": true,
      "local_path": "attachments/4f1cac52c2dc/images/001.jpeg",
      "src": "https://static.simonwillison.net/static/2023/wordcamp-llms/llm-work-for-you.056.jpeg",
      "alt": "A location in 1,536 dimension space  There's a 3D plot with 400 red dots arranged randomly across 3 axis.",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://simonwillison.net/",
      "label": "Simon Willison’s Weblog",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/newsletters/",
      "label": "Newsletter",
      "type": "a"
    },
    {
      "href": "https://fandf.co/4hJeKAm",
      "label": "Upgrade to Pomerium's agentic access management trusted by NVIDIA and OpenAI",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/",
      "label": "LLM",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/en/stable/changelog.html",
      "label": "LLM 0.9",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Sep/4/llm-embeddings/",
      "label": "Embeddings",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Oct/23/embeddings/",
      "label": "Embeddings: What they are and why they matter",
      "type": "a"
    },
    {
      "href": "https://platform.openai.com/docs/guides/embeddings",
      "label": "OpenAI embedding model",
      "type": "a"
    },
    {
      "href": "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2",
      "label": "all-MiniLM-L6-v2",
      "type": "a"
    },
    {
      "href": "https://til.simonwillison.net/",
      "label": "my TIL site",
      "type": "a"
    },
    {
      "href": "https://til.simonwillison.net/llms/openai-embeddings-related-content",
      "label": "Storing and serving related documents with openai-to-sqlite and embeddings",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Aug/27/wordcamp-llms/",
      "label": "here",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/llm-sentence-transformers",
      "label": "llm-sentence-transformers",
      "type": "a"
    },
    {
      "href": "https://www.sbert.net/",
      "label": "sentence-transformers",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/en/stable/setup.html",
      "label": "installed LLM already",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/en/stable/logging.html",
      "label": "store prompts and responses",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/en/stable/embeddings/cli.html",
      "label": "in detail in the documentation",
      "type": "a"
    },
    {
      "href": "https://datasette.io/tools/apple-notes-to-sqlite",
      "label": "apple-notes-to-sqlite",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/en/stable/embeddings/python-api.html",
      "label": "from the documentation",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/llm-cluster",
      "label": "llm-cluster",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/llm/issues",
      "label": "LLM issues",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/paginate-json",
      "label": "paginate-json",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Aug/1/llama-2-mac/",
      "label": "running on my own laptop",
      "type": "a"
    },
    {
      "href": "https://github.com/facebookresearch/faiss",
      "label": "FAISS",
      "type": "a"
    },
    {
      "href": "https://github.com/nmslib/hnswlib",
      "label": "hnswlib",
      "type": "a"
    },
    {
      "href": "https://github.com/asg017/sqlite-vss",
      "label": "sqlite-vss",
      "type": "a"
    },
    {
      "href": "https://github.com/pgvector/pgvector",
      "label": "pgvector",
      "type": "a"
    },
    {
      "href": "https://www.pinecone.io/",
      "label": "Pinecone",
      "type": "a"
    },
    {
      "href": "https://milvus.io/",
      "label": "Milvus",
      "type": "a"
    },
    {
      "href": "https://datasette.io/plugins/datasette-faiss",
      "label": "datasette-faiss",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/llm/issues/216",
      "label": "issue #216",
      "type": "a"
    },
    {
      "href": "https://datasette.io/discord-llm",
      "label": "Join the #llm Discord",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/en/stable/plugins/directory.html",
      "label": "12 plugins already",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/llm-mlc",
      "label": "llm-mlc",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/llm-gpt4all",
      "label": "llm-gpt4all",
      "type": "a"
    },
    {
      "href": "https://github.com/simonw/llm-llama-cpp",
      "label": "llm-llama-cpp",
      "type": "a"
    },
    {
      "href": "https://llm.datasette.io/en/stable/plugins/tutorial-model-plugin.html",
      "label": "building a plugin",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Sep/4/",
      "label": "4th September 2023",
      "type": "a"
    },
    {
      "href": "https://fedi.simonwillison.net/@simon",
      "label": "Mastodon",
      "type": "a"
    },
    {
      "href": "https://bsky.app/profile/simonwillison.net",
      "label": "Bluesky",
      "type": "a"
    },
    {
      "href": "https://twitter.com/simonw",
      "label": "Twitter",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/about/",
      "label": "subscribe to my newsletter",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2026/Oct/12/python-3-15/",
      "label": "Hello Python 3.15, goodbye 3.10, and now I can use 3.11",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2026/Oct/9/built-using-my-voice/",
      "label": "A new feature for my blog, built using my voice",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2026/Oct/7/claude-haiku-5-5/",
      "label": "Claude Haiku 5.5",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2026/Oct/3/default-hard-budget-caps/",
      "label": "We're going to need default hard budget caps on pretty much everything",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/series/llm-releases/",
      "label": "New releases of LLM",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/May/18/cli-tools-for-llms/",
      "label": "llm, ttok and strip-tags - CLI tools for working with ChatGPT and other LLMs",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Jul/12/llm/",
      "label": "My LLM CLI tool now supports self-hosted language models via plugins",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2024/Jan/26/llm/",
      "label": "LLM 0.13: The annotated release notes",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2024/Oct/29/llm-multi-modal/",
      "label": "You can now run prompts against images, audio and video in your terminal using LLM",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2025/Feb/17/llm/",
      "label": "LLM 0.22, the annotated release notes",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2025/Feb/28/llm-schemas/",
      "label": "Structured data extraction from unstructured content using LLM schemas",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/cli/",
      "label": "cli 109",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/open-source/",
      "label": "open-source 322",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/projects/",
      "label": "projects 558",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/sqlite/",
      "label": "sqlite 488",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/ai/",
      "label": "ai 2,279",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/generative-ai/",
      "label": "generative-ai 2,021",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/vector-search/",
      "label": "vector-search 21",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/llms/",
      "label": "llms 1,987",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/embeddings/",
      "label": "embeddings 62",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/llm/",
      "label": "llm 639",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/tags/rag/",
      "label": "rag 35",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Sep/12/llm-clip-and-chat/",
      "label": "Build an image search engine with llm-clip, chat with models with llm chat",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/Aug/30/datasette-plus-weeknotes/",
      "label": "Datasette 1.0a4 and 1.0a5, plus weeknotes",
      "type": "a"
    },
    {
      "href": "https://github.com/sponsors/simonw/",
      "label": "Sponsor & subscribe",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2002/",
      "label": "2002",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2003/",
      "label": "2003",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2004/",
      "label": "2004",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2005/",
      "label": "2005",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2006/",
      "label": "2006",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2007/",
      "label": "2007",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2008/",
      "label": "2008",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2009/",
      "label": "2009",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2010/",
      "label": "2010",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2011/",
      "label": "2011",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2012/",
      "label": "2012",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2013/",
      "label": "2013",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2014/",
      "label": "2014",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2015/",
      "label": "2015",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2016/",
      "label": "2016",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2017/",
      "label": "2017",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2018/",
      "label": "2018",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2019/",
      "label": "2019",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2020/",
      "label": "2020",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2021/",
      "label": "2021",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2022/",
      "label": "2022",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2023/",
      "label": "2023",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2024/",
      "label": "2024",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2025/",
      "label": "2025",
      "type": "a"
    },
    {
      "href": "https://simonwillison.net/2026/",
      "label": "2026",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:47:03Z",
  "source_url": "https://simonwillison.net/2023/Sep/4/llm-embeddings/",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 21575,
  "expected_body_images": 2,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/4f1cac52c2dc/pages/source.html",
  "local_html": "attachments/4f1cac52c2dc/pages/article.html",
  "body_base_path": "attachments/4f1cac52c2dc/pages/",
  "body_markdown_path": "attachments/4f1cac52c2dc/pages/article.md"
}