<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Faith Wachukwu</title>
    <description>The latest articles on DEV Community by Faith Wachukwu (@faithkovi).</description>
    <link>https://dev.to/faithkovi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F244639%2F4f2aeffa-07f3-4824-b640-e476c70b9961.png</url>
      <title>DEV Community: Faith Wachukwu</title>
      <link>https://dev.to/faithkovi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faithkovi"/>
    <language>en</language>
    <item>
      <title>Airbyte vs Estuary for Postgres to Snowflake Replication</title>
      <dc:creator>Faith Wachukwu</dc:creator>
      <pubDate>Tue, 10 Nov 2026 20:31:11 +0000</pubDate>
      <link>https://dev.to/faithkovi/airbyte-vs-estuary-for-postgres-to-snowflake-replication-51dp</link>
      <guid>https://dev.to/faithkovi/airbyte-vs-estuary-for-postgres-to-snowflake-replication-51dp</guid>
      <description>&lt;p&gt;Estuary is the pick when Postgres changes have to land in Snowflake as they happen and one capture has to feed several destinations. &lt;a href="https://docs.estuary.dev/reference/Connectors/capture-connectors/PostgreSQL/" rel="noopener noreferrer"&gt;Estuary&lt;/a&gt; streams changes continuously and can push a Snowflake materialization as fast as possible. &lt;a href="https://docs.airbyte.com/platform/using-airbyte/core-concepts/sync-schedules" rel="noopener noreferrer"&gt;Airbyte&lt;/a&gt; runs on a schedule: as often as every 5 minutes on Pro and Enterprise Flex, one destination per CDC source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estuary vs Airbyte for Postgres to Snowflake: Overview
&lt;/h2&gt;

&lt;p&gt;Estuary captures Postgres changes continuously. Airbyte syncs on a chosen schedule.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.estuary.dev/reference/Connectors/capture-connectors/PostgreSQL/" rel="noopener noreferrer"&gt;Estuary's Postgres connector&lt;/a&gt; uses change data capture to continuously capture updates from a Postgres database into one or more data collections. &lt;a href="https://estuary.dev/blog/postgres-to-snowflake/" rel="noopener noreferrer"&gt;Estuary’s Snowflake&lt;/a&gt; connector then uploads the changes to a Snowflake table stage before applying them to the target table in a transaction. Both standard and delta updates are supported, and &lt;a href="https://docs.snowflake.com/en/user-guide/snowpipe-streaming/data-load-snowpipe-streaming-overview" rel="noopener noreferrer"&gt;Snowpipe Streaming&lt;/a&gt; is available for the delta bindings.&lt;/p&gt;

&lt;p&gt;Snowpipe Streaming is the lowest-latency method &lt;a href="https://docs.estuary.dev/reference/Connectors/materialization-connectors/Snowflake/" rel="noopener noreferrer"&gt;Estuary&lt;/a&gt; has for loading Snowflake, and it is the default for delta update bindings. Estuary documents exactly-once delivery for every transaction that commits on that path.&lt;/p&gt;

&lt;p&gt;That fits teams that need Snowflake tables to track Postgres closely and would rather not run a sync engine to get there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.airbyte.com/integrations/sources/postgres" rel="noopener noreferrer"&gt;Airbyte's Postgres connector&lt;/a&gt; reads the write-ahead log with logical replication to capture deletes. Its &lt;a href="https://docs.airbyte.com/integrations/destinations/snowflake" rel="noopener noreferrer"&gt;Snowflake destination&lt;/a&gt; loads data through Snowflake’s internal stage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.airbyte.com/platform/using-airbyte/getting-started/oss-quickstart" rel="noopener noreferrer"&gt;Airbyte&lt;/a&gt; fits teams whose downstream consumers tolerate a scheduled cadence, and teams that want to run the sync engine themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estuary vs Airbyte Pricing for Postgres to Snowflake: pricing options; &lt;a href="https://airbyte.com/pricing" rel="noopener noreferrer"&gt;shortest scheduled interval: 1 hour on Standard, 15 minutes on Plus, 5 minutes on Pro and Enterprise Flex&lt;/a&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primary key on every incrementally synced table; reset and resync after a schema change&lt;/th&gt;
&lt;th&gt;Snowflake internal stage&lt;/th&gt;
&lt;th&gt;
&lt;a href="https://docs.airbyte.com/platform/cloud/managing-airbyte-cloud/manage-credits" rel="noopener noreferrer"&gt;Four credits per GB&lt;/a&gt; from a database source on Standard and Plus, as of October 2026&lt;/th&gt;
&lt;th&gt;One CDC source to one destination&lt;/th&gt;
&lt;th&gt;Airbyte Cloud, or self-managed Airbyte Core&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Estuary's row is a set of settings you choose. Airbyte's is a set of intervals you schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freshness: how often each tool refreshes Snowflake
&lt;/h2&gt;

&lt;p&gt;Freshness decides this pair, and it is the one property you cannot buy your way out of later.&lt;/p&gt;

&lt;p&gt;Estuary's Snowflake materialization supports a sync schedule. Its other warehouse materializations do too, including Databricks, BigQuery, Amazon Redshift, MotherDuck, ClickHouse, and Azure Fabric Warehouse. Set it to 0s, and the materialization always pushes updated data as fast as possible.&lt;/p&gt;

&lt;p&gt;Airbyte gives each connection three options that allow a sync to run. Airbyte Cloud caps those intervals by plan. &lt;a href="https://airbyte.com/pricing" rel="noopener noreferrer"&gt;The shortest scheduled interval is 1 hour on Standard, 15 minutes on Plus, and 5 minutes on Pro and Enterprise Flex&lt;/a&gt;. For both interval-based and cron schedules, Airbyte documents a &lt;a href="https://docs.airbyte.com/platform/using-airbyte/core-concepts/sync-schedules" rel="noopener noreferrer"&gt;±30-minute window for initiating a sync&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your consumers read Snowflake every few minutes, either cadence is fine. If they need updates pushed as fast as possible, use Estuary's Sync Frequency of 0s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres source demands and schema behavior
&lt;/h2&gt;

&lt;p&gt;Estuary can automatically add new columns to Snowflake without resetting the pipeline when the columns meet the connector’s criteria, and recommended fields are enabled. Airbyte’s Postgres CDC connector requires a primary key on each table synced incrementally and recommends resetting and resyncing the affected data after a source schema change. For tables without a primary key, Airbyte recommends a full refresh.&lt;/p&gt;

&lt;p&gt;Estuary captures from Postgres standby servers on version 16 or later. Each additional capture against the same database needs its own replication slot.&lt;/p&gt;

&lt;p&gt;Airbyte can also run Postgres CDC from a read replica of the primary. It supports that starting at Postgres version 16.1 and connector version 3.6.21, provided the replica is configured to allow it. Airbyte also notes that logical replication increases disk space used on the database server.&lt;/p&gt;

&lt;p&gt;Estuary normally uses a watermark table during backfills. Read-only capture avoids those writes, but at least one captured table must change regularly. If none does, include a heartbeat table and update it every few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost at Postgres-to-Snowflake volume
&lt;/h2&gt;

&lt;p&gt;The two tools meter different things: &lt;a href="https://estuary.dev/pricing/" rel="noopener noreferrer"&gt;Estuary&lt;/a&gt; charges per gigabyte and per instance, while &lt;a href="https://airbyte.com/pricing" rel="noopener noreferrer"&gt;Airbyte&lt;/a&gt; bills by volume or capacity.&lt;/p&gt;

&lt;p&gt;Estuary's Cloud plan costs $0.50 per GB of data, as of October 2026. Connector instances cost $100 each per month, dropping to $50 for each instance after your sixth. Data is counted each time it is captured and each time it is delivered to a destination, including data produced by transformations. Estuary's own example prices 100 GB a month from Postgres to Snowflake at $300.&lt;/p&gt;

&lt;p&gt;Airbyte splits its plans by billing model: Standard and Plus bill per use, while Pro and Enterprise Flex are priced by capacity. Airbyte Standard costs $20 a month for the first 5 credits and $5 per additional credit. If this is the only pipeline on the plan, the monthly bill is $1,995: $20 plus $5 for each of the remaining 395 credits.&lt;/p&gt;

&lt;p&gt;Airbyte bills the serialized volume it measures, and it warns that this is likely larger than what a direct query against your database shows. The capacity plans go the other way: Airbyte says Pro and Enterprise Flex carry no charges based on data volume.&lt;/p&gt;

&lt;p&gt;For the same 100 GB Postgres-to-Snowflake pipeline, Estuary Cloud costs $300 a month, compared with $1,995 on Airbyte Standard. Both of its rates are published, so a Postgres to Snowflake pipeline is arithmetic you can do today. Airbyte's credit plans meter a volume that can exceed the data you can see. Its capacity plans trade volume charges for fixed capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fan-out, operations, and deployment
&lt;/h2&gt;

&lt;p&gt;One Postgres database, several consumers. This is where the two tools stop looking alike.&lt;/p&gt;

&lt;p&gt;Estuary allows one-to-many replication, so a single source can feed warehouses, search systems, microservices, and AI pipelines at the same time. Estuary stores captured data in an Estuary-managed bucket by default, or in your own bucket if you configure one. A new destination can read the retained data without reading it again from Postgres. &lt;/p&gt;

&lt;p&gt;Estuary offers more than 200 source and destination connectors, and it offers private and BYOC deployments on annual contracts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.airbyte.com/integrations/sources/postgres/postgres-troubleshooting" rel="noopener noreferrer"&gt;Airbyte's Postgres CDC notes&lt;/a&gt; put a hard limit on fan-out. An Airbyte database source using CDC replication can be used with only a single Airbyte destination. That is the constraint to check against your own destination list before you pick a tool.&lt;/p&gt;

&lt;p&gt;On Cloud Pro and Enterprise Flex, newly triggered syncs are queued when all committed data workers are in use. Airbyte lists 700 or more connectors across its plans. Airbyte Core is Airbyte's open-source product, which you deploy locally.&lt;/p&gt;

&lt;p&gt;Estuary wins on fan-out, because one capture reaches many destinations from change data it already stored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict: pick Airbyte when, pick Estuary when
&lt;/h2&gt;

&lt;p&gt;Pick Estuary when changes have to be queryable in Snowflake as they happen, and when one Postgres source has to feed more than one destination. Estuary captures changes continuously and pushes them at a frequency you set. Additional destinations can reuse the data Estuary has already captured and retained.&lt;/p&gt;

&lt;p&gt;Pick Airbyte when you want to run the sync engine yourself, or when a scheduled cadence is enough, and each CDC source has one destination. Airbyte Core is the product to look at for that, since you deploy a local instance yourself.&lt;/p&gt;

&lt;p&gt;For continuous Postgres to Snowflake replication, Estuary is the pick. Its bill is per gigabyte captured and delivered, plus per active connector instance, as of October 2026.&lt;/p&gt;

&lt;p&gt;Measure how many gigabytes you capture and deliver in a month, then estimate the cost using Estuary’s published rates. Count how many destinations need the same Postgres changes. With Airbyte Postgres CDC, each additional destination needs a separately configured source that reads those changes again. Estuary captures them once, and each destination can reuse the retained data.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Airbyte support Postgres CDC into Snowflake?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.airbyte.com/integrations/sources/postgres" rel="noopener noreferrer"&gt;Airbyte's Postgres connector&lt;/a&gt; supports change data capture and replication using the xmin system column. &lt;a href="https://docs.airbyte.com/integrations/destinations/snowflake" rel="noopener noreferrer"&gt;Its Snowflake destination loads&lt;/a&gt; data through Snowflake's internal stage and supports automatic schema evolution. The CDC path reads the Postgres write-ahead log with logical replication to capture deletes.&lt;/p&gt;

</description>
      <category>database</category>
      <category>dataengineering</category>
      <category>postgres</category>
      <category>streaming</category>
    </item>
    <item>
      <title>How to Choose Embedding and Reranking Models for Domain-Specific Search</title>
      <dc:creator>Faith Wachukwu</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:24:33 +0000</pubDate>
      <link>https://dev.to/faithkovi/how-to-choose-embedding-and-reranking-models-for-domain-specific-search-8a6</link>
      <guid>https://dev.to/faithkovi/how-to-choose-embedding-and-reranking-models-for-domain-specific-search-8a6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Answer:&lt;/strong&gt; Choose embedding and reranking models for domain search on a labelled set of your own queries, not on leaderboard averages. Shortlist by licence, context length, language, and size, measure recall@k for each embedder on your corpus, then add a reranker and keep it only if the measured gain justifies its latency. Run that bake-off on Superlinked Inference Engine: one API for the shortlist, on SIE Cloud or self-hosted, with open weights and shared GPUs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A lawyer types "indemnification cap" into the contract search your team shipped last quarter. The top result is a well-written paragraph about indemnification in general, from the right kind of agreement, with no cap in it. The clause that actually limits liability to twelve months of fees sits at rank fourteen, below the fold and below the lawyer's patience. The embedder behind it scores near the top of the public leaderboard average. That average, it turns out, was never asked about your documents, which is a common oversight for a number that gets quoted in so many design reviews.&lt;/p&gt;

&lt;p&gt;This is the ordinary failure of domain search. General-purpose models learn topic similarity very well. Domain queries often need something narrower: the defined term, the specific obligation, the number that qualifies the clause. A model can find the right neighbourhood and still return the wrong house.&lt;/p&gt;

&lt;p&gt;The method below picks models for that job. It sits upstream of a full retrieval evaluation, which the legal retrieval evaluation guide in this series covers in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Build the query set from real queries
&lt;/h2&gt;

&lt;p&gt;Start with what people actually type. Pull queries from search logs, support tickets, or the questions subject experts ask each other. Invented queries are cleaner than real ones, and that is exactly the problem: real queries are short, ambiguous, and full of the domain's shorthand.&lt;/p&gt;

&lt;p&gt;For each query, record the documents or passages that answer it. This is the labelled set, and it is the most valuable artefact in the whole exercise, because every later decision is measured against it. Keep the labels at the level you retrieve at. If the index stores clauses, label clauses, not whole contracts.&lt;/p&gt;

&lt;p&gt;Include the hard cases on purpose. Near-duplicate boilerplate, defined terms with a special meaning, and queries where the correct answer is a number all belong in the set. A query set of easy queries will rate every model highly and tell you nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Read the leaderboard for the right subset
&lt;/h2&gt;

&lt;p&gt;Public benchmarks are still the right way to build a shortlist, as long as you read the part that matches your task. MTEB, the Massive Text Embedding Benchmark, covers retrieval, reranking, classification, clustering, and semantic similarity, and it publishes variants such as MTEB(eng, v2) along with domain-specific benchmarks for legal, medical, and code text.&lt;/p&gt;

&lt;p&gt;The overall average blends all of those tasks. A model that excels at clustering and similarity can sit high on the average while being ordinary at retrieval. For a search system, sort by the retrieval columns, and then by any domain benchmark that resembles your corpus. Treat the result as a list of candidates, not as a ranking you can trust. Open models you can serve on SIE belong on that shortlist: embedders and rerankers from the catalog, measured on your queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Shortlist by hard constraints
&lt;/h2&gt;

&lt;p&gt;Before any testing, remove the models you could not ship. Four constraints do most of the work:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Question to answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;Does the model licence permit your commercial use?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context length&lt;/td&gt;
&lt;td&gt;Does your chunk size fit without truncation?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;Does it cover every language in the corpus and in the queries?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size and cost&lt;/td&gt;
&lt;td&gt;Can you serve it at your query volume and latency target?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three to five embedders and two or three rerankers is a manageable shortlist. More than that and the bake-off turns into a research project. Include one general-purpose baseline, so you can tell whether a domain model is actually earning its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test each embedder with recall@k
&lt;/h2&gt;

&lt;p&gt;Embed the corpus with each candidate and run the labelled queries. Recall@k asks a simple question: of the passages that answer the query, how many appear in the top k results? Pick k to match what the next stage sees. If a reranker will look at the top 50, measure recall@50 for the embedder, because the reranker cannot rescue a passage that never reached it.&lt;/p&gt;

&lt;p&gt;Look past the average. Split the results by query type and read the failures by hand. A model that wins overall but loses every numeric query may be the wrong choice for a contracts product. Note latency and index size alongside recall, since a larger embedding dimension costs storage for every document you will ever index; models trained Matryoshka-style can be truncated to fewer dimensions, and it is worth measuring recall at the truncated size too.&lt;/p&gt;

&lt;p&gt;One setting quietly ruins more bake-offs than any model choice: query instructions. E5 models expect a "query:" or "passage:" prefix, and Qwen3 embedders take a task instruction on the query side. Run each candidate the way its model card says, or the comparison measures your preprocessing, not the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Add a reranker and measure the change
&lt;/h2&gt;

&lt;p&gt;A reranker, usually a cross-encoder, reads the query and each candidate passage together and scores the pair; late-interaction and listwise rerankers exist too. It is slower than vector search, so it only sees the top of the list, and it often fixes exactly the failure in the opening scene: the right topic ranked above the right answer.&lt;/p&gt;

&lt;p&gt;Measure what the user sees. Rerank the top k from each embedder and compare nDCG@10, which rewards the right passage near the top, before and after; MTEB's retrieval tasks report the same metric. Record the reranker's p95 latency at your k as well, because scoring fifty pairs per query is the most expensive thing on the query path. The table below shows the shape of the result you are looking for. The figures are illustrative, invented to show the method, and they describe no real model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate (illustrative)&lt;/th&gt;
&lt;th&gt;nDCG@10, embedder only&lt;/th&gt;
&lt;th&gt;nDCG@10, with reranker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;General-purpose embedder A&lt;/td&gt;
&lt;td&gt;0.61&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain-tuned embedder B&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;0.76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smaller embedder C&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a result shaped like this, the reranker narrows the gap between embedders, and the smaller model plus a reranker may beat the larger model alone, at lower serving cost once the reranker's per-query cost is counted. Only your own numbers can say whether that happens on your corpus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Decide the representation, and price the reindex
&lt;/h2&gt;

&lt;p&gt;Dense vectors capture meaning. Sparse representations, such as SPLADE-style models, keep exact term matching, which helps with part numbers, statute references, and defined terms. Hybrid search combines the two. Multi-vector models, in the ColBERT family, keep one vector per token and score with late interaction, at the cost of a larger index.&lt;/p&gt;

&lt;p&gt;Test the representation that addresses your observed failures, not the most sophisticated one. Then price the decision honestly. Changing the embedder later means re-embedding every document. Stores such as Qdrant and Weaviate let you add the new vector beside the old one in the same collection, which makes the cutover gentler, but the embedding run itself is unavoidable. A reranker, by contrast, can be swapped without touching the index at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fli3qfi74je5f03opm95p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fli3qfi74je5f03opm95p.png" alt="Superlinked Inference Engine" width="800" height="360"&gt;&lt;/a&gt;&lt;em&gt;Superlinked Inference Engine&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If candidates are hosted APIs, the bake-off is a loop over API keys. If you self-host them, serving five or six models long enough to test them can dominate the work. SIE, &lt;a href="https://superlinked.com/" rel="noopener noreferrer"&gt;Superlinked&lt;/a&gt;'s inference engine, shortens that step. Models load on demand and are evicted least-recently-used when GPU memory fills, so switching candidates is a change of model ID in the request rather than a new deployment.&lt;/p&gt;

&lt;p&gt;On SIE Cloud, the managed product, a bake-off can use the Cloud models directly: Qwen3-Embedding-4B, Snowflake's arctic-embed-l-v2.0, bge-m3 in sparse and multi-vector form, GTE-ModernColBERT, and the 0.6B and 4B Qwen3 rerankers, each priced per model on the &lt;a href="https://superlinked.com/serverless" rel="noopener noreferrer"&gt;Cloud pricing&lt;/a&gt; table. Self-hosted, the wider &lt;a href="https://superlinked.com/models" rel="noopener noreferrer"&gt;model catalog&lt;/a&gt; adds candidates such as gte-modernbert-base, multilingual-e5-large-instruct, bge-reranker-v2-m3, and mxbai-rerank-large-v2, with a documented path to add other Hugging Face models by config file. Self-hosting is also the mode to choose when the evaluation corpus cannot leave your environment.&lt;/p&gt;

&lt;p&gt;The same function scores every candidate. gold maps each query ID to the set of passage indices that answer it; for a real corpus, batch the passage encoding and store the vectors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sie_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SIEClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SIEClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-sie-…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.superlinked.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dense&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;])])&lt;/span&gt;
    &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="n"&gt;is_query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dense&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;))))[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-Reranker-4B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidate_ids&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-Embedding-4B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Snowflake/snowflake-arctic-embed-l-v2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feed each embedder's top 50 into rerank and compute nDCG@10 on the result to fill the Step 5 table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest take:&lt;/strong&gt; SIE is the production layer for the embedders and rerankers this bake-off is choosing, and for the OCR and extraction models a domain corpus often needs beside them. One API runs every candidate, on SIE Cloud or self-hosted, with open weights and GPUs shared across the shortlist, so a new model is a model ID rather than a new deployment. The labelled query set is still yours to build. Once the numbers are in, the embedder and reranker you keep stay on that same API, in the cloud or on your own cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How large must the labelled set be before the numbers mean anything?
&lt;/h3&gt;

&lt;p&gt;Enough to separate the candidates you care about. A few dozen queries will expose large differences, such as a model that fails every numeric query. Distinguishing two strong embedders usually takes a few hundred, spread across query types in proportion to real traffic. Label by the unit you retrieve, include hard negatives, and keep the set fixed while you compare models. Add queries later from production failures, since those are the cases the first set missed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should we fine-tune the embedder or just add a reranker?
&lt;/h3&gt;

&lt;p&gt;Add the reranker first. It needs no training data beyond the evaluation set, it can be removed in a single change, and it does not force a reindex. Fine-tuning an embedder can outperform it on a stable, well-labelled domain, but it brings a training pipeline, a new model version to maintain, and a full re-embedding run every time the model changes. Consider fine-tuning when a reranker has plateaued and your labelled set is large enough to train on without overfitting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not use OpenAI embeddings and move on?
&lt;/h3&gt;

&lt;p&gt;Domain queries fail in the way the opening scene describes: a general model ranks the right topic above the right answer. Run the bake-off. Specialist embedders and rerankers on SIE, open weights on one API, are the production layer for that test and for the models you keep. SIE Cloud and self-hosted use the same SDK, so the candidate that passes the labelled set stays where you measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing, then committing
&lt;/h2&gt;

&lt;p&gt;Model choice for domain search is a measurement problem. Build the query set from real traffic, shortlist from the right benchmark columns, test recall on your corpus, add a reranker, and only then settle the representation and the reindex. Run the bake-off on SIE Cloud; the same code runs against a self-hosted cluster for the models its catalog carries.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>superlinked</category>
      <category>search</category>
    </item>
  </channel>
</rss>
