@memberjunction/web-search-engine 0.0.1-seed.1 → 6.2.0-edge.0
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.
- package/LICENSE +183 -0
- package/README.md +343 -3
- package/dist/BaseWebSearchProvider.d.ts +89 -0
- package/dist/BaseWebSearchProvider.d.ts.map +1 -0
- package/dist/BaseWebSearchProvider.js +130 -0
- package/dist/BaseWebSearchProvider.js.map +1 -0
- package/dist/WebSearchEngine.d.ts +107 -0
- package/dist/WebSearchEngine.d.ts.map +1 -0
- package/dist/WebSearchEngine.js +396 -0
- package/dist/WebSearchEngine.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/operations/WebSearchQueryOperation.d.ts +8 -0
- package/dist/operations/WebSearchQueryOperation.d.ts.map +1 -0
- package/dist/operations/WebSearchQueryOperation.js +72 -0
- package/dist/operations/WebSearchQueryOperation.js.map +1 -0
- package/dist/providers/BraveWebSearchProvider.d.ts +41 -0
- package/dist/providers/BraveWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/BraveWebSearchProvider.js +169 -0
- package/dist/providers/BraveWebSearchProvider.js.map +1 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.d.ts +45 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.js +116 -0
- package/dist/providers/DuckDuckGoWebSearchProvider.js.map +1 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.d.ts +36 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.js +125 -0
- package/dist/providers/GoogleCustomSearchWebSearchProvider.js.map +1 -0
- package/dist/providers/PerplexityWebSearchProvider.d.ts +40 -0
- package/dist/providers/PerplexityWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/PerplexityWebSearchProvider.js +148 -0
- package/dist/providers/PerplexityWebSearchProvider.js.map +1 -0
- package/dist/providers/TavilyWebSearchProvider.d.ts +30 -0
- package/dist/providers/TavilyWebSearchProvider.d.ts.map +1 -0
- package/dist/providers/TavilyWebSearchProvider.js +126 -0
- package/dist/providers/TavilyWebSearchProvider.js.map +1 -0
- package/dist/providers/httpFailure.d.ts +29 -0
- package/dist/providers/httpFailure.d.ts.map +1 -0
- package/dist/providers/httpFailure.js +136 -0
- package/dist/providers/httpFailure.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2024 MariaDB plc, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB plc.
|
|
5
|
+
|
|
6
|
+
-----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
|
|
10
|
+
Licensor: Blue Cypress, Inc.
|
|
11
|
+
|
|
12
|
+
Licensed Work: MemberJunction.
|
|
13
|
+
The Licensed Work is (c) 2023-2026 Blue Cypress, Inc.
|
|
14
|
+
|
|
15
|
+
Additional Use Grant: Subject to the terms of this License, Licensor grants
|
|
16
|
+
you the following additional rights to make Production
|
|
17
|
+
Use of the Licensed Work.
|
|
18
|
+
|
|
19
|
+
1. Internal Use
|
|
20
|
+
|
|
21
|
+
You may make production use of the Licensed Work for
|
|
22
|
+
your own internal business or organizational operations.
|
|
23
|
+
|
|
24
|
+
2. Nonprofit Use
|
|
25
|
+
|
|
26
|
+
If you are a Nonprofit, you may make production use of
|
|
27
|
+
the Licensed Work for the operations and activities of
|
|
28
|
+
your Organizational Family.
|
|
29
|
+
|
|
30
|
+
3. MemberJunction Certified Program Use
|
|
31
|
+
|
|
32
|
+
If you are authorized by Licensor under the
|
|
33
|
+
MemberJunction Certified Program to provide professional
|
|
34
|
+
services using the Licensed Work, you may make
|
|
35
|
+
production use of the Licensed Work in providing such
|
|
36
|
+
professional services to a client, provided that:
|
|
37
|
+
|
|
38
|
+
(a) the Licensed Work is deployed in, and the applicable
|
|
39
|
+
production use occurs within, an environment owned,
|
|
40
|
+
leased, licensed, subscribed to, or otherwise controlled
|
|
41
|
+
by that client; and
|
|
42
|
+
|
|
43
|
+
(b) the production use is for that client's own internal
|
|
44
|
+
business or organizational operations or is otherwise
|
|
45
|
+
independently permitted to that client under this
|
|
46
|
+
Additional Use Grant.
|
|
47
|
+
|
|
48
|
+
4. Definitions Applicable to the Additional Use Grant
|
|
49
|
+
|
|
50
|
+
"Affiliate" means, with respect to a specified Person,
|
|
51
|
+
any other Person that directly or indirectly Controls,
|
|
52
|
+
is Controlled by, or is under common Control with such
|
|
53
|
+
specified Person.
|
|
54
|
+
|
|
55
|
+
"Control" (including the terms "Controls," "Controlled
|
|
56
|
+
by," and "under common Control with") means the direct
|
|
57
|
+
or indirect possession of the power to direct or cause
|
|
58
|
+
the direction of the management and policies of a
|
|
59
|
+
Person, whether through ownership of voting interests,
|
|
60
|
+
by contract, or otherwise.
|
|
61
|
+
|
|
62
|
+
"Organizational Family" means, with respect to a Person,
|
|
63
|
+
(a) such Person and its Affiliates, and (b) any
|
|
64
|
+
nonprofit organization, governmental entity, chapter,
|
|
65
|
+
division, local affiliate, regional affiliate, state
|
|
66
|
+
affiliate, national affiliate, or other entity that is
|
|
67
|
+
formally affiliated with such Person through governing
|
|
68
|
+
documents, a charter, bylaws, a membership agreement, or
|
|
69
|
+
another written organizational instrument, and is
|
|
70
|
+
recognized under such documents as part of the same
|
|
71
|
+
organizational structure.
|
|
72
|
+
|
|
73
|
+
"Nonprofit" means a Person recognized by the Internal
|
|
74
|
+
Revenue Service as exempt from federal income taxation
|
|
75
|
+
under Section 501(c)(3), 501(c)(4), 501(c)(5), or
|
|
76
|
+
501(c)(6) of the Internal Revenue Code, or a foreign
|
|
77
|
+
organization recognized under substantially equivalent
|
|
78
|
+
laws.
|
|
79
|
+
|
|
80
|
+
A Person claiming eligibility as a Nonprofit shall, upon
|
|
81
|
+
Licensor's reasonable request, provide documentation
|
|
82
|
+
reasonably sufficient to demonstrate that it qualifies
|
|
83
|
+
as a Nonprofit. If such Person materially misrepresents,
|
|
84
|
+
or is unable to demonstrate, its qualification as a
|
|
85
|
+
Nonprofit, the rights granted to such Person under
|
|
86
|
+
Section 2 of this Additional Use Grant shall terminate.
|
|
87
|
+
|
|
88
|
+
"MemberJunction Certified Program" means Licensor's
|
|
89
|
+
then-current program for certifying and authorizing a
|
|
90
|
+
Person to provide professional services using the
|
|
91
|
+
Licensed Work.
|
|
92
|
+
|
|
93
|
+
"Person" means any individual, corporation, limited
|
|
94
|
+
liability company, partnership, association, nonprofit
|
|
95
|
+
organization, governmental entity, or other legal or
|
|
96
|
+
organizational entity.
|
|
97
|
+
|
|
98
|
+
Change Date: Four (4) years from the date the Licensed Work is first
|
|
99
|
+
made available.
|
|
100
|
+
|
|
101
|
+
Change License: MIT License.
|
|
102
|
+
|
|
103
|
+
For information about alternative licensing arrangements for the Licensed
|
|
104
|
+
Work, please contact Blue Cypress, Inc.
|
|
105
|
+
|
|
106
|
+
-----------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
Terms
|
|
109
|
+
|
|
110
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
111
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
112
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
113
|
+
production use.
|
|
114
|
+
|
|
115
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
116
|
+
available distribution of a specific version of the Licensed Work under this
|
|
117
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
118
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
119
|
+
above terminate.
|
|
120
|
+
|
|
121
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
122
|
+
currently in effect as described in this License, you must purchase a
|
|
123
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
124
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
125
|
+
|
|
126
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
127
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
128
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
129
|
+
for each version of the Licensed Work released by Licensor.
|
|
130
|
+
|
|
131
|
+
You must conspicuously display this License on each original or modified copy
|
|
132
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
133
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
134
|
+
License apply to your use of that work.
|
|
135
|
+
|
|
136
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
137
|
+
terminate your rights under this License for the current and all other
|
|
138
|
+
versions of the Licensed Work.
|
|
139
|
+
|
|
140
|
+
This License does not grant you any right in any trademark or logo of
|
|
141
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
142
|
+
Licensor as expressly required by this License).
|
|
143
|
+
|
|
144
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
145
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
146
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
147
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
148
|
+
TITLE.
|
|
149
|
+
|
|
150
|
+
MariaDB hereby grants you permission to use this License's text to license
|
|
151
|
+
your works, and to refer to it using the trademark "Business Source License",
|
|
152
|
+
as long as you comply with the Covenants of Licensor below.
|
|
153
|
+
|
|
154
|
+
-----------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
Covenants of Licensor
|
|
157
|
+
|
|
158
|
+
In consideration of the right to use this License's text and the "Business
|
|
159
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
160
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
161
|
+
|
|
162
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
|
163
|
+
or a license that is compatible with GPL Version 2.0 or a later version,
|
|
164
|
+
where "compatible" means that software provided under the Change License
|
|
165
|
+
can be included in a program with software provided under GPL Version 2.0
|
|
166
|
+
or a later version. Licensor may specify additional Change Licenses without
|
|
167
|
+
limitation.
|
|
168
|
+
|
|
169
|
+
2. To either: (a) specify an additional grant of rights to use that does not
|
|
170
|
+
impose any additional restriction on the right granted in this License, as
|
|
171
|
+
the Additional Use Grant; or (b) insert the text "None".
|
|
172
|
+
|
|
173
|
+
3. To specify a Change Date.
|
|
174
|
+
|
|
175
|
+
4. Not to modify this License in any other way.
|
|
176
|
+
|
|
177
|
+
-----------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
Notice
|
|
180
|
+
|
|
181
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
182
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
183
|
+
under an Open Source License, as stated in this License.
|
package/README.md
CHANGED
|
@@ -1,4 +1,344 @@
|
|
|
1
1
|
# @memberjunction/web-search-engine
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
|
|
3
|
+
Metadata-driven **web search** for MemberJunction: a set of external search vendors behind one
|
|
4
|
+
interface, with administrator-controlled priority and automatic failover.
|
|
5
|
+
|
|
6
|
+
The administrator picks the vendor. The agent never does.
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import { WebSearchEngine } from '@memberjunction/web-search-engine';
|
|
10
|
+
|
|
11
|
+
await WebSearchEngine.Instance.Config(false, contextUser);
|
|
12
|
+
const result = await WebSearchEngine.Instance.Search({ Query: 'association management trends' }, contextUser);
|
|
13
|
+
|
|
14
|
+
if (result.Success) {
|
|
15
|
+
for (const hit of result.Hits) console.log(hit.Title, hit.URL);
|
|
16
|
+
console.log('served by', result.ProviderUsed);
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Why this exists
|
|
23
|
+
|
|
24
|
+
Google discontinues the Custom Search JSON API on **2027-01-01** and has already closed it to new
|
|
25
|
+
customers. Microsoft retired the Bing Search APIs on **2025-08-11**. Both replaced a commodity SERP
|
|
26
|
+
API with an answer-shaped grounding product locked to their own agent stack. Selling raw ranked web
|
|
27
|
+
results to developers is a business both companies have chosen to exit.
|
|
28
|
+
|
|
29
|
+
The lesson is not "pick a different vendor". It is that **any single web-search dependency has an
|
|
30
|
+
expiry date**, so the architecture has to make the next shutdown a configuration change.
|
|
31
|
+
|
|
32
|
+
Before this package, MJ had four independent web-search Actions — Google, Tavily, Perplexity,
|
|
33
|
+
DuckDuckGo — with three incompatible output contracts (`Items`, `Results`, `Content`). Nothing could
|
|
34
|
+
transparently fall back onto a different shape, so "Perplexity as a backup" was never actually a
|
|
35
|
+
backup. Swapping vendors meant editing every agent that named one.
|
|
36
|
+
|
|
37
|
+
Now it is a row in a table.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## How it differs from `@memberjunction/search-engine`
|
|
42
|
+
|
|
43
|
+
They sound alike and are not interchangeable. **`search-engine` searches content MJ owns**;
|
|
44
|
+
this package searches the open web.
|
|
45
|
+
|
|
46
|
+
| | `search-engine` | `web-search-engine` |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Searches | entity records, vectors, full-text, storage files | the public web, via external vendors |
|
|
49
|
+
| Result identity | `EntityName` + `RecordID` — a row you can open | a URL |
|
|
50
|
+
| Result type | `SearchResultItem` | `WebSearchHit` |
|
|
51
|
+
| Multiple sources | queried **together**, fused with RRF | tried **in order**, first success wins |
|
|
52
|
+
| Cost per query | a database query | real money at a vendor |
|
|
53
|
+
| Failure mode | a slow query | a rate limit, an expired key, a discontinued API |
|
|
54
|
+
| Provider table | `__mj.SearchProvider` | `__mj.WebSearchProvider` |
|
|
55
|
+
|
|
56
|
+
The decisive detail is the result contract: `SearchResultItem` **requires** `ID`, `EntityName` and
|
|
57
|
+
`RecordID` — the primary key of a source record — and the pipeline ends in `SearchEnricher`, which
|
|
58
|
+
resolves entity icons and record display names by entity lookup. A web hit has none of those.
|
|
59
|
+
Fitting one in would mean lying in the type and dragging it through enrichment designed to decorate
|
|
60
|
+
entity records.
|
|
61
|
+
|
|
62
|
+
What the two **do** share is the plugin pattern, and this package reuses all of it: a provider table
|
|
63
|
+
carrying `DriverClass` / `Priority` / `Status` / `CredentialID`, ClassFactory resolution, and the
|
|
64
|
+
`Initialize()` → `CheckAvailability()` → `IsAvailable()` lifecycle.
|
|
65
|
+
|
|
66
|
+
> **Blending the two** — web results and internal records in one ranked list — is reachable through
|
|
67
|
+
> the existing RRF fusion, but it needs a result type that does not require `EntityName`. That is a
|
|
68
|
+
> deliberate future step, not a side effect of this package.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Three ways in
|
|
73
|
+
|
|
74
|
+
Pick by who is calling.
|
|
75
|
+
|
|
76
|
+
| Caller | Use | Why |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Server-side TypeScript | `WebSearchEngine.Instance.Search()` | Direct, typed, no indirection |
|
|
79
|
+
| Browser / Angular | the `WebSearch.Query` Remote Operation | Keys never leave the server; typed end to end |
|
|
80
|
+
| An AI agent | the `Web Search` **Action** | Discoverable from metadata, one tool instead of four |
|
|
81
|
+
|
|
82
|
+
All three land in the same engine and obey the same provider configuration.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Who uses this
|
|
87
|
+
|
|
88
|
+
**No agent or skill binds a search vendor directly.** Every agent and skill that does web lookups — including the `Web Research` skill — binds the provider-neutral `Web Search` Action, and leaves `Provider` unset so the engine can fail over.
|
|
89
|
+
|
|
90
|
+
`Google Custom Search` and `Perplexity Search` remain registered for direct or manual execution, but carry **no agent or skill bindings**. Anything new binds `Web Search`, never a vendor.
|
|
91
|
+
|
|
92
|
+
Verify at any time:
|
|
93
|
+
```bash
|
|
94
|
+
grep -rn "Google Custom Search\|Perplexity Search" metadata/agents/ metadata/ai-skills/
|
|
95
|
+
# no output = no vendor bindings remain
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Selection: how a provider is chosen
|
|
101
|
+
|
|
102
|
+
### Without an explicit provider
|
|
103
|
+
|
|
104
|
+
1. Filter to providers that are `Active`, report themselves available, and can do what was asked
|
|
105
|
+
2. Order by `Priority` **ascending — lower wins**
|
|
106
|
+
3. Try each in turn until one succeeds
|
|
107
|
+
|
|
108
|
+
Failover is **conditional**, and the condition is the part that matters:
|
|
109
|
+
|
|
110
|
+
| Failure | Engine does | Why |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `transient` — rate limit, 5xx, timeout, bad key | try the next provider | another vendor may well succeed |
|
|
113
|
+
| `permanent` — the request itself is rejected | **stop** | every other vendor will reject it too; retrying costs four more paid calls and buries the real error behind four identical ones |
|
|
114
|
+
|
|
115
|
+
**Zero hits is a success.** A narrow query legitimately matches nothing. Treating that as failure
|
|
116
|
+
makes the engine retry a query that will keep returning nothing, at four vendors' expense.
|
|
117
|
+
|
|
118
|
+
### With an explicit provider
|
|
119
|
+
|
|
120
|
+
`Provider` pins the search to one vendor, by `Name` or `DriverClass`, case-insensitively. There is
|
|
121
|
+
**no failover** — the call fails, with a code that says exactly why:
|
|
122
|
+
|
|
123
|
+
| Code | Meaning | Fix |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `PROVIDER_NOT_FOUND` | no such provider in metadata | add the row |
|
|
126
|
+
| `PROVIDER_NOT_ACTIVE` | the row exists, `Status` is not `Active` | flip the status |
|
|
127
|
+
| `PROVIDER_UNAVAILABLE` | Active, but not operational — usually no credential | set the key |
|
|
128
|
+
| `PROVIDER_LACKS_CAPABILITY` | e.g. `IncludeAnswer` on a hits-only provider | ask a different provider, or drop the flag |
|
|
129
|
+
|
|
130
|
+
Those are four different problems with four different fixes, which is why they are four codes and
|
|
131
|
+
not one. And the reason a pinned provider never silently falls back: a substitution is invisible.
|
|
132
|
+
Results keep arriving, from a vendor nobody chose, until something eventually surfaces it — usually
|
|
133
|
+
an invoice.
|
|
134
|
+
|
|
135
|
+
### `Attempts` — read this one
|
|
136
|
+
|
|
137
|
+
Every provider tried is recorded in `result.Attempts`, **including on the success path**.
|
|
138
|
+
|
|
139
|
+
If the primary provider rate-limits every call and the secondary quietly serves everything, the
|
|
140
|
+
system looks completely healthy. Nothing else makes that visible while the spend moves to a vendor
|
|
141
|
+
nobody selected.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const result = await WebSearchEngine.Instance.Search({ Query: 'q' }, user);
|
|
145
|
+
// result.ProviderUsed === 'Tavily'
|
|
146
|
+
// result.Attempts === [
|
|
147
|
+
// { ProviderName: 'Brave', Succeeded: false, FailureKind: 'transient', ErrorMessage: '429 …' },
|
|
148
|
+
// { ProviderName: 'Tavily', Succeeded: true, HitCount: 10 },
|
|
149
|
+
// ]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Configuring providers
|
|
155
|
+
|
|
156
|
+
Rows live in `__mj.WebSearchProvider`, seeded from `metadata/web-search-providers/`.
|
|
157
|
+
|
|
158
|
+
| Column | Purpose |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `Name` | Admin-facing name, and what `Provider` matches. Unique |
|
|
161
|
+
| `DriverClass` | ClassFactory key — `@RegisterClass(BaseWebSearchProvider, …)` |
|
|
162
|
+
| `Status` | `Pending` / `Active` / `Terminated`. Only `Active` loads |
|
|
163
|
+
| `Priority` | **Lower is tried first** |
|
|
164
|
+
| `CredentialID` | FK to `Credential`; falls back to an env var when null |
|
|
165
|
+
| `ProviderConfig` | JSON of non-secret driver settings |
|
|
166
|
+
| `MaxResultsOverride` | Per-provider cap for pay-per-query vendors |
|
|
167
|
+
| `AllowResultCaching` | Defaults to **0**. See *Caching rights*, below |
|
|
168
|
+
| `DisplayName` / `Icon` / `Comments` | Admin UI and notes |
|
|
169
|
+
|
|
170
|
+
**Capabilities are not columns.** Whether a driver can synthesize an answer or honour a domain
|
|
171
|
+
filter is a property of the *implementation*, declared on the class — exactly as `search-engine`
|
|
172
|
+
declares `SourceType`. The table holds configuration: on/off, order, credentials, limits.
|
|
173
|
+
|
|
174
|
+
To change the primary vendor, edit one row's `Priority` and run `mj sync push`. No code change, no
|
|
175
|
+
agent change, no redeploy.
|
|
176
|
+
|
|
177
|
+
### Bundled drivers
|
|
178
|
+
|
|
179
|
+
| Driver | Default priority | Answer | Notes |
|
|
180
|
+
|---|---|---|---|
|
|
181
|
+
| `BraveWebSearchProvider` | 10 | no | **Own index** — not a Google or Bing reseller, so it fails independently of everything else here. Default primary |
|
|
182
|
+
| `TavilyWebSearchProvider` | 20 | yes | Returns extracted page content rather than snippets plus links. Aggregator |
|
|
183
|
+
| `PerplexityWebSearchProvider` | 30 | yes | Hits from the raw `/search` endpoint; Sonar only when an answer is asked for |
|
|
184
|
+
| `GoogleCustomSearchWebSearchProvider` | 40 | no | **Discontinued 2027-01-01.** Present so existing keys keep working through the transition |
|
|
185
|
+
| `DuckDuckGoWebSearchProvider` | 90 | no | Keyless last resort, Instant Answer API only. Answers a minority of queries — treat reliance on it as *unconfigured*. No HTML fallback, [by design](#why-duckduckgo-has-no-html-fallback) |
|
|
186
|
+
|
|
187
|
+
### Why DuckDuckGo has no HTML fallback
|
|
188
|
+
|
|
189
|
+
An earlier revision fell back to fetching and regex-parsing DuckDuckGo's HTML results page, which
|
|
190
|
+
answered far more queries than the Instant Answer API does. It was removed deliberately.
|
|
191
|
+
|
|
192
|
+
Running regular expressions over remote HTML is running them over **attacker-influenceable input**,
|
|
193
|
+
and CodeQL flagged the parser with two high-severity findings: polynomial backtracking in the result
|
|
194
|
+
patterns (the `[^"]*…[^"]*` shape) and incomplete sanitization in its tag-stripping.
|
|
195
|
+
|
|
196
|
+
Hardening those patterns would have *narrowed* the surface; deleting the parser *removes* it. For a
|
|
197
|
+
fallback inside a last-resort provider this engine already tells operators not to rely on, the
|
|
198
|
+
capability was not worth the exposure — and "the regex looks safe now" is exactly the kind of claim
|
|
199
|
+
that is true when written and wrong later.
|
|
200
|
+
|
|
201
|
+
**If you need real web coverage, configure a provider with an API.** That is what the rest of this
|
|
202
|
+
package is for.
|
|
203
|
+
|
|
204
|
+
### Credentials
|
|
205
|
+
|
|
206
|
+
Each driver reads its key from the linked `Credential` record first, then falls back to an
|
|
207
|
+
environment variable:
|
|
208
|
+
|
|
209
|
+
| Driver | Credential key(s) | Environment fallback |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| Brave | `apiKey` | `BRAVE_SEARCH_API_KEY` |
|
|
212
|
+
| Tavily | `apiKey` | `TAVILY_API_KEY` |
|
|
213
|
+
| Perplexity | `apiKey` | `PERPLEXITY_API_KEY` |
|
|
214
|
+
| Google Custom Search | `apiKey`, `cx` | `GOOGLE_CUSTOM_SEARCH_API_KEY`, `GOOGLE_CUSTOM_SEARCH_CX` |
|
|
215
|
+
| DuckDuckGo | — | — |
|
|
216
|
+
|
|
217
|
+
The fallback exists so a host upgrading from the per-vendor Actions does not have to migrate its
|
|
218
|
+
secrets into the Credential store on the same day it gains the engine.
|
|
219
|
+
|
|
220
|
+
A driver with no key **self-disables** rather than throwing. An unconfigured vendor is a normal
|
|
221
|
+
state in a product shipped to many hosts, not an error — the engine simply routes around it.
|
|
222
|
+
|
|
223
|
+
### ⚠️ Caching rights
|
|
224
|
+
|
|
225
|
+
`AllowResultCaching` defaults to `0`, and that default is deliberate.
|
|
226
|
+
|
|
227
|
+
Caching rights differ sharply between vendors: some sell result storage as a distinct plan tier,
|
|
228
|
+
others forbid persistent caching outright, and at least one major grounding product's terms prohibit
|
|
229
|
+
caching, syndicating or analysing results at all. Violating any of it is **silent** — nothing fails,
|
|
230
|
+
you are simply in breach.
|
|
231
|
+
|
|
232
|
+
Nothing in this engine caches today. The column exists so that the first caching layer reads a
|
|
233
|
+
per-provider gate instead of inventing one, and so the contractual answer lives next to the
|
|
234
|
+
credential it applies to. **Confirm your plan's terms before setting it to 1.**
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Writing a driver
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
242
|
+
import { BaseWebSearchProvider, WebSearchCapabilities } from '@memberjunction/web-search-engine';
|
|
243
|
+
|
|
244
|
+
@RegisterClass(BaseWebSearchProvider, 'AcmeWebSearchProvider')
|
|
245
|
+
export class AcmeWebSearchProvider extends BaseWebSearchProvider {
|
|
246
|
+
public readonly Capabilities: WebSearchCapabilities = {
|
|
247
|
+
Answer: false, DomainFilter: true, Freshness: true, Region: true, MaxResultsCap: 25,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
private apiKey: string | undefined;
|
|
251
|
+
|
|
252
|
+
public async CheckAvailability(): Promise<void> {
|
|
253
|
+
this.apiKey = this.GetSecret('apiKey', 'ACME_API_KEY');
|
|
254
|
+
this.apiKey ? this.MarkAvailable() : this.MarkUnavailable('No Acme API key.');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public async ExecuteSearch(params: WebSearchParams): Promise<WebSearchProviderResponse> {
|
|
258
|
+
try {
|
|
259
|
+
const response = await HttpGet<AcmeResponse>(ENDPOINT, { /* … */ });
|
|
260
|
+
return { Success: true, Hits: (response.Data?.results ?? []).map(toHit) };
|
|
261
|
+
} catch (e) {
|
|
262
|
+
return classifyHttpFailure(e, 'Acme'); // sets FailureKind for you
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Four rules, each of which has a failure mode if ignored:
|
|
269
|
+
|
|
270
|
+
1. **Declare capabilities honestly.** Claiming `DomainFilter: true` and then ignoring the filter
|
|
271
|
+
returns confident results for a different question than the one asked.
|
|
272
|
+
2. **Classify failures correctly.** `classifyHttpFailure` handles the common cases; the distinction
|
|
273
|
+
that matters is 400/422 (`permanent`) versus everything else (`transient`).
|
|
274
|
+
3. **Self-disable, don't throw**, when the driver is simply not configured on this host.
|
|
275
|
+
4. **Export a `Load…()` anchor** and call it from the package index. Bundlers drop a module whose
|
|
276
|
+
exports are unused, and a dropped module never runs its decorator — so the driver silently
|
|
277
|
+
vanishes from `ClassFactory` and the engine reports "no registered driver" for a `DriverClass`
|
|
278
|
+
that plainly exists in the source.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Result codes
|
|
283
|
+
|
|
284
|
+
| Code | `Success` | Meaning |
|
|
285
|
+
|---|:---:|---|
|
|
286
|
+
| `SUCCESS` | ✅ | Served. `Hits` may legitimately be empty |
|
|
287
|
+
| `MISSING_QUERY` | ❌ | `Query` was blank |
|
|
288
|
+
| `NO_PROVIDERS_CONFIGURED` | ❌ | No provider rows at all — a setup problem, not a search failure |
|
|
289
|
+
| `PROVIDER_LOAD_FAILED` | ❌ | The provider list could not be **read** — query failed, entity missing from metadata, or no permission. Distinct from the row above on purpose: a failed read also leaves the list empty, and telling an operator to "add a record" when the read never succeeded sends them to fix configuration that is fine |
|
|
290
|
+
| `NO_ELIGIBLE_PROVIDER` | ❌ | Providers exist, none is available and capable of this request |
|
|
291
|
+
| `PROVIDER_NOT_FOUND` / `_NOT_ACTIVE` / `_UNAVAILABLE` / `_LACKS_CAPABILITY` | ❌ | An explicit `Provider` could not serve — see the table above |
|
|
292
|
+
| `ALL_PROVIDERS_FAILED` | ❌ | Every eligible provider was tried; see `Attempts` |
|
|
293
|
+
| `INVALID_REQUEST` | ❌ | The request itself was rejected; retrying elsewhere cannot help |
|
|
294
|
+
|
|
295
|
+
`Search()` **never throws** for a search-level failure — branch on `Success`, as with `RunView`.
|
|
296
|
+
Exceptions escaping a driver are caught, recorded as a transient attempt, and failed over.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Setup after the migration
|
|
301
|
+
|
|
302
|
+
The migration, its CodeGen tail and the Remote Operation's server half are all committed, so a
|
|
303
|
+
local database only needs the schema applied and the metadata seeded:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
pnpm run mj:migrate # creates __mj.WebSearchProvider
|
|
307
|
+
pnpm mj sync push --dir metadata # seeds the provider rows, the Action, the scopes
|
|
308
|
+
pnpm install && pnpm run build
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Migrate before pushing metadata.** `metadata/web-search-providers/` writes into
|
|
312
|
+
`__mj.WebSearchProvider`, so the table has to exist first. (A different ordering constraint —
|
|
313
|
+
metadata before `mj codegen`, because remote operations generate from rows rather than schema —
|
|
314
|
+
applies when regenerating, not when applying what is already committed.)
|
|
315
|
+
|
|
316
|
+
Verify before committing:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
node .github/scripts/check-migration-entityfield-sequence.mjs # apply-time MAX(Sequence)+1, never a literal
|
|
320
|
+
npm run check:codegen-tail # generated entity/resolvers/form are committed
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The PostgreSQL counterpart is **not** authored per-PR — conversion is build-engineer work at release
|
|
324
|
+
time.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Testing
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
cd packages/WebSearchEngine && pnpm test
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The selection tests are the ones that matter. Each pins a property with a way of passing while
|
|
335
|
+
broken: conditional failover, an explicit provider never being substituted, and `Attempts` being
|
|
336
|
+
recorded on the **success** path.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Related
|
|
341
|
+
|
|
342
|
+
- [`guides/SEARCH_OVERVIEW_GUIDE.md`](../../guides/SEARCH_OVERVIEW_GUIDE.md) — which search API to reach for
|
|
343
|
+
- [`@memberjunction/search-engine`](../SearchEngine) — internal search over content MJ owns
|
|
344
|
+
- [`packages/Actions/CLAUDE.md`](../Actions/CLAUDE.md) — why the Action is a thin wrapper
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Base class for web search drivers.
|
|
3
|
+
*
|
|
4
|
+
* Each driver wraps one external search vendor and normalises its response into
|
|
5
|
+
* {@link WebSearchHit}. Drivers are discovered at runtime via `@RegisterClass` and the
|
|
6
|
+
* `MJ: Web Search Providers` metadata entity, whose `DriverClass` column is the registration key.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/web-search-engine
|
|
9
|
+
*/
|
|
10
|
+
import { UserInfo } from '@memberjunction/core';
|
|
11
|
+
import { RegisteredWebSearchProviderInfo, WebSearchCapabilities, WebSearchParams, WebSearchProviderConfig, WebSearchProviderResponse } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Abstract base for a web search driver.
|
|
14
|
+
*
|
|
15
|
+
* Subclasses register with `@RegisterClass(BaseWebSearchProvider, 'DriverClassName')` to become
|
|
16
|
+
* discoverable by {@link WebSearchEngine}.
|
|
17
|
+
*
|
|
18
|
+
* Lifecycle, per provider record, at engine config time:
|
|
19
|
+
* 1. `WebSearchEngine` loads Active `MJ: Web Search Providers` records
|
|
20
|
+
* 2. ClassFactory creates an instance from `DriverClass`
|
|
21
|
+
* 3. {@link Initialize} receives the record's configuration and any decrypted credential values
|
|
22
|
+
* 4. {@link CheckAvailability} verifies the driver is actually operational
|
|
23
|
+
* 5. If {@link IsAvailable} is true, the driver joins the selection pool
|
|
24
|
+
*
|
|
25
|
+
* A driver that cannot work — no API key, missing optional dependency — must **self-disable in
|
|
26
|
+
* `CheckAvailability` rather than throw**. An unconfigured vendor is a normal state in a product
|
|
27
|
+
* shipped to many hosts, not an error, and the engine simply routes around it.
|
|
28
|
+
*/
|
|
29
|
+
export declare abstract class BaseWebSearchProvider {
|
|
30
|
+
/** What this driver can do. Declared by the class, never read from the database. */
|
|
31
|
+
abstract readonly Capabilities: WebSearchCapabilities;
|
|
32
|
+
/** Configuration from the metadata record, set during {@link Initialize}. */
|
|
33
|
+
protected config: WebSearchProviderConfig | null;
|
|
34
|
+
private _available;
|
|
35
|
+
private _unavailableReason;
|
|
36
|
+
/**
|
|
37
|
+
* Receive configuration from the provider's metadata record.
|
|
38
|
+
*
|
|
39
|
+
* The default stores it; override to do additional setup, and call `super.Initialize(...)`.
|
|
40
|
+
*/
|
|
41
|
+
Initialize(config: WebSearchProviderConfig, _contextUser: UserInfo): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Determine whether this driver can serve requests, calling {@link MarkAvailable} or
|
|
44
|
+
* {@link MarkUnavailable} exactly once.
|
|
45
|
+
*
|
|
46
|
+
* Must not throw for the ordinary "not configured here" case — see the class note.
|
|
47
|
+
*/
|
|
48
|
+
abstract CheckAvailability(contextUser: UserInfo): Promise<void>;
|
|
49
|
+
/** Whether this driver may participate in searches. False until `CheckAvailability` says otherwise. */
|
|
50
|
+
IsAvailable(): boolean;
|
|
51
|
+
/** Why this driver is unavailable, for diagnostics. Null when it is available. */
|
|
52
|
+
get UnavailableReason(): string | null;
|
|
53
|
+
/** Execute a search. Called only when {@link IsAvailable} is true. */
|
|
54
|
+
abstract ExecuteSearch(params: WebSearchParams, contextUser: UserInfo): Promise<WebSearchProviderResponse>;
|
|
55
|
+
/** Mark this driver operational. Call from {@link CheckAvailability}. */
|
|
56
|
+
protected MarkAvailable(): void;
|
|
57
|
+
/** Mark this driver unusable, with a reason an administrator can act on. */
|
|
58
|
+
protected MarkUnavailable(reason: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Read a credential value, preferring the decrypted `MJ: Credentials` record and falling back
|
|
61
|
+
* to an environment variable.
|
|
62
|
+
*
|
|
63
|
+
* The fallback exists because the web search actions this engine supersedes read their keys
|
|
64
|
+
* from `mj.config.cjs` / the environment, and a host upgrading should not have to migrate its
|
|
65
|
+
* secrets into the Credential store on the same day it gains the engine.
|
|
66
|
+
*
|
|
67
|
+
* @param credentialKey - key within the credential record's decrypted values, e.g. `apiKey`
|
|
68
|
+
* @param envVarName - environment variable consulted when no credential value is present
|
|
69
|
+
*/
|
|
70
|
+
protected GetSecret(credentialKey: string, envVarName: string): string | undefined;
|
|
71
|
+
/** Read a non-secret setting from the record's `ProviderConfig` JSON. */
|
|
72
|
+
protected GetConfigValue<T>(key: string): T | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The effective result cap: the caller's request, bounded by the metadata override (when set)
|
|
75
|
+
* and then by the vendor's own hard cap.
|
|
76
|
+
*
|
|
77
|
+
* Clamped rather than rejected — a caller asking for 50 wants as many as possible, and most
|
|
78
|
+
* vendors reject the whole request when asked for more than they serve.
|
|
79
|
+
*/
|
|
80
|
+
protected ResolveMaxResults(requested: number | undefined, fallback?: number): number;
|
|
81
|
+
/**
|
|
82
|
+
* Enumerate every registered driver with its capabilities, for admin UI dropdowns.
|
|
83
|
+
*
|
|
84
|
+
* Instantiates each registration to read `Capabilities`, so a driver's constructor must stay
|
|
85
|
+
* side-effect free.
|
|
86
|
+
*/
|
|
87
|
+
static GetAvailableProviders(): RegisteredWebSearchProviderInfo[];
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=BaseWebSearchProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseWebSearchProvider.d.ts","sourceRoot":"","sources":["../src/BaseWebSearchProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAY,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EACH,+BAA+B,EAC/B,qBAAqB,EACrB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EAC5B,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;GAgBG;AACH,8BAAsB,qBAAqB;IACvC,oFAAoF;IACpF,kBAAyB,YAAY,EAAE,qBAAqB,CAAC;IAE7D,6EAA6E;IAC7E,SAAS,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAQ;IAExD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,kBAAkB,CAAuB;IAEjD;;;;OAIG;IACU,UAAU,CAAC,MAAM,EAAE,uBAAuB,EAAE,YAAY,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/F;;;;;OAKG;aACa,iBAAiB,CAAC,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvE,uGAAuG;IAChG,WAAW,IAAI,OAAO;IAI7B,kFAAkF;IAClF,IAAW,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAE5C;IAED,sEAAsE;aACtD,aAAa,CACzB,MAAM,EAAE,eAAe,EACvB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,yBAAyB,CAAC;IAErC,yEAAyE;IACzE,SAAS,CAAC,aAAa,IAAI,IAAI;IAK/B,4EAA4E;IAC5E,SAAS,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK/C;;;;;;;;;;OAUG;IACH,SAAS,CAAC,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IASlF,yEAAyE;IACzE,SAAS,CAAC,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAKvD;;;;;;OAMG;IACH,SAAS,CAAC,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,SAAK,GAAG,MAAM;IAMjF;;;;;OAKG;WACW,qBAAqB,IAAI,+BAA+B,EAAE;CA+B3E"}
|