Improving coverage of Global Search

This commit is contained in:
David Bomba 2025-09-01 18:10:37 +10:00
parent 8231d967dd
commit 2795faa0fe
15 changed files with 166 additions and 56 deletions

View File

@ -19,6 +19,7 @@ use App\Models\Invoice;
use App\Models\Project;
use Elastic\Elasticsearch\ClientBuilder;
use App\Http\Requests\Search\GenericSearchRequest;
use Illuminate\Support\Str;
class SearchController extends Controller
{
@ -44,6 +45,8 @@ class SearchController extends Controller
private array $projects = [];
private array $tasks = [];
public function __invoke(GenericSearchRequest $request)
{
if (config('scout.driver') == 'elastic') {
@ -87,17 +90,41 @@ class SearchController extends Controller
$params = [
// 'index' => 'clients,invoices,client_contacts',
// 'index' => 'clients,invoices,client_contacts,quotes,expenses,credits,recurring_invoices,vendors,vendor_contacts,purchase_orders,projects',
'index' => 'clients_v2,invoices_v2,client_contacts_v2,quotes_v2,expenses_v2,credits_v2,recurring_invoices_v2,vendors_v2,vendor_contacts_v2,purchase_orders_v2,projects_v2',
'index' => 'clients_v2,invoices_v2,client_contacts_v2,quotes_v2,expenses_v2,credits_v2,recurring_invoices_v2,vendors_v2,vendor_contacts_v2,purchase_orders_v2,projects_v2,tasks_v2',
'body' => [
'query' => [
'bool' => [
'must' => [
'should' => [
[
'multi_match' => [
'query' => $search,
'fields' => ['*'],
'fuzziness' => 'AUTO',
]
],
// Safe nested search that won't fail on missing fields
[
'nested' => [
'path' => 'line_items',
'query' => [
'multi_match' => [
'query' => $search,
'fields' => [
'line_items.product_key^2',
'line_items.notes^2',
'line_items.custom_value1',
'line_items.custom_value2',
'line_items.custom_value3',
'line_items.custom_value4'
],
'fuzziness' => 'AUTO',
]
],
'ignore_unmapped' => true
]
],
],
'minimum_should_match' => 1,
'filter' => [
'match' => [
'company_key' => $company->company_key,
@ -109,8 +136,11 @@ class SearchController extends Controller
],
];
$results = $elastic->search($params);
nlog($results['hits']);
$this->mapResults($results['hits']['hits'] ?? []);
return response()->json([
@ -125,6 +155,7 @@ class SearchController extends Controller
'vendor_contacts' => $this->vendor_contacts,
'purchase_orders' => $this->purchase_orders,
'projects' => $this->projects,
'tasks' => $this->tasks,
'settings' => $this->settingsMap(),
], 200);
@ -134,8 +165,8 @@ class SearchController extends Controller
{
foreach ($results as $result) {
switch ($result['_index']) {
case 'clients':
switch (true) {
case Str::startsWith($result['_index'], 'clients'):
if ($result['_source']['is_deleted']) { //do not return deleted results
break;
@ -149,7 +180,7 @@ class SearchController extends Controller
];
break;
case 'invoices':
case Str::startsWith($result['_index'], 'invoices'):
if ($result['_source']['is_deleted']) { //do not return deleted invoices
break;
@ -163,7 +194,7 @@ class SearchController extends Controller
'path' => "/invoices/{$result['_source']['hashed_id']}/edit"
];
break;
case 'client_contacts':
case Str::startsWith($result['_index'], 'client_contacts'):
if ($result['_source']['__soft_deleted']) {
break;
@ -176,7 +207,7 @@ class SearchController extends Controller
'path' => "/clients/{$result['_source']['client_id']}"
];
break;
case 'quotes':
case Str::startsWith($result['_index'], 'quotes'):
if ($result['_source']['__soft_deleted']) {
break;
@ -191,7 +222,7 @@ class SearchController extends Controller
break;
case 'expenses':
case Str::startsWith($result['_index'], 'expenses'):
if ($result['_source']['__soft_deleted']) {
break;
@ -206,7 +237,7 @@ class SearchController extends Controller
break;
case 'credits':
case Str::startsWith($result['_index'], 'credits'):
if ($result['_source']['__soft_deleted']) {
break;
@ -221,7 +252,7 @@ class SearchController extends Controller
break;
case 'recurring_invoices':
case Str::startsWith($result['_index'], 'recurring_invoices'):
if ($result['_source']['__soft_deleted']) {
break;
@ -236,7 +267,7 @@ class SearchController extends Controller
break;
case 'vendors':
case Str::startsWith($result['_index'], 'vendors'):
if ($result['_source']['__soft_deleted']) {
break;
@ -251,7 +282,7 @@ class SearchController extends Controller
break;
case 'vendor_contacts':
case Str::startsWith($result['_index'], 'vendor_contacts'):
if ($result['_source']['__soft_deleted']) {
break;
@ -266,7 +297,7 @@ class SearchController extends Controller
break;
case 'purchase_orders':
case Str::startsWith($result['_index'], 'purchase_orders'):
if ($result['_source']['__soft_deleted']) {
break;
@ -281,7 +312,7 @@ class SearchController extends Controller
break;
case 'projects':
case Str::startsWith($result['_index'], 'projects'):
if ($result['_source']['__soft_deleted']) {
break;
@ -294,6 +325,20 @@ class SearchController extends Controller
'path' => "/projects/{$result['_source']['hashed_id']}"
];
break;
case Str::startsWith($result['_index'], 'tasks'):
if ($result['_source']['is_deleted']) {
break;
}
$this->tasks[] = [
'name' => $result['_source']['name'],
'type' => '/task',
'id' => $result['_source']['hashed_id'],
'path' => "/tasks/{$result['_source']['hashed_id']}/edit"
];
break;
}
}

View File

@ -267,9 +267,9 @@ class Client extends BaseModel implements HasLocalePreference
return [
'id' => $this->company->db.":".$this->id,
'name' => $name,
'is_deleted' => $this->is_deleted,
'is_deleted' => (bool)$this->is_deleted,
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'number' => (string)$this->number,
'id_number' => $this->id_number,
'vat_number' => $this->vat_number,
'balance' => $this->balance,

View File

@ -169,6 +169,11 @@ class ClientContact extends Authenticatable implements HasLocalePreference
'email',
];
public function searchableAs(): string
{
return 'client_contacts_v2';
}
public function toSearchableArray()
{
return [

View File

@ -122,7 +122,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property \App\Models\Client $client
* @property \App\Models\Vendor|null $vendor
* @property-read \App\Models\Location|null $location
* @property-read mixed $pivot
* @property-read mixed $pivotcredi
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\CompanyLedger> $company_ledger
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
@ -223,8 +223,8 @@ class Credit extends BaseModel
'id' => $this->company->db.":".$this->id,
'name' => ctrans('texts.credit') . " " . $this->number . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'number' => (string)$this->number,
'is_deleted' => (bool)$this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,

View File

@ -187,8 +187,8 @@ class Expense extends BaseModel
'id' => $this->company->db.":".$this->id,
'name' => ctrans('texts.expense') . " " . ($this->number ?? '') . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'number' => (string)$this->number,
'is_deleted' => (bool)$this->is_deleted,
'amount' => (float) $this->amount,
'date' => $this->date ?? null,
'custom_value1' => (string)$this->custom_value1,
@ -196,6 +196,8 @@ class Expense extends BaseModel
'custom_value3' => (string)$this->custom_value3,
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'public_notes' => (string)$this->public_notes,
'private_notes' => (string)$this->private_notes
];
}

View File

@ -269,8 +269,8 @@ class Invoice extends BaseModel
'id' => (string)$this->company->db.":".$this->id,
'name' => ctrans('texts.invoice') . " " . $this->number . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'number' => (string)$this->number,
'is_deleted' => (bool)$this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,

View File

@ -116,8 +116,8 @@ class Project extends BaseModel
'id' => (string)$this->company->db.":".$this->id,
'name' => ctrans('texts.project') . " " . $this->number . ' | ' . $this->name . " | " . $this->client->present()->name(),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'number' => (string)$this->number,
'is_deleted' => (bool)$this->is_deleted,
'task_rate' => (float) $this->task_rate,
'budgeted_hours' => (float) $this->budgeted_hours,
'due_date' => $this->due_date,

View File

@ -228,8 +228,8 @@ class PurchaseOrder extends BaseModel
'id' => $this->company->db.":".$this->id,
'name' => ctrans('texts.purchase_order') . " " . $this->number . " | " . $this->vendor->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'number' => (string)$this->number,
'is_deleted' => (bool)$this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,

View File

@ -220,8 +220,8 @@ class Quote extends BaseModel
'id' => $this->company->db.":".$this->id,
'name' => ctrans('texts.quote') . " " . ($this->number ?? '') . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'number' => (string)$this->number,
'is_deleted' => (bool)$this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,

View File

@ -138,15 +138,6 @@ class RecurringInvoice extends BaseModel
use PresentableTrait;
use Searchable;
/**
* Get the index name for the model.
*
* @return string
*/
public function searchableAs(): string
{
return 'recurring_invoices_v2';
}
protected $presenter = RecurringInvoicePresenter::class;
@ -280,17 +271,65 @@ class RecurringInvoice extends BaseModel
'remaining_cycles',
];
/**
* Get the index name for the model.
*
* @return string
*/
public function searchableAs(): string
{
return 'recurring_invoices_v2';
}
public function toSearchableArray()
{
$locale = $this->company->locale();
App::setLocale($locale);
// Properly cast line items to ensure correct types
$line_items = [];
if ($this->line_items) {
foreach ($this->line_items as $item) {
$line_items[] = [
'quantity' => (float)($item->quantity ?? 0),
'net_cost' => (float)($item->net_cost ?? 0),
'cost' => (float)($item->cost ?? 0),
'product_key' => (string)($item->product_key ?? ''),
'product_cost' => (float)($item->product_cost ?? 0),
'notes' => (string)($item->notes ?? ''),
'discount' => (float)($item->discount ?? 0),
'is_amount_discount' => (bool)($item->is_amount_discount ?? false),
'tax_name1' => (string)($item->tax_name1 ?? ''),
'tax_rate1' => (float)($item->tax_rate1 ?? 0),
'tax_name2' => (string)($item->tax_name2 ?? ''),
'tax_rate2' => (float)($item->tax_rate2 ?? 0),
'tax_name3' => (string)($item->tax_name3 ?? ''),
'tax_rate3' => (float)($item->tax_rate3 ?? 0),
'sort_id' => (string)($item->sort_id ?? ''),
'line_total' => (float)($item->line_total ?? 0),
'gross_line_total' => (float)($item->gross_line_total ?? 0),
'tax_amount' => (float)($item->tax_amount ?? 0),
'date' => (string)($item->date ?? ''),
'custom_value1' => (string)($item->custom_value1 ?? ''),
'custom_value2' => (string)($item->custom_value2 ?? ''),
'custom_value3' => (string)($item->custom_value3 ?? ''),
'custom_value4' => (string)($item->custom_value4 ?? ''),
'type_id' => (string)($item->type_id ?? ''),
'tax_id' => (string)($item->tax_id ?? ''),
'task_id' => (string)($item->task_id ?? ''),
'expense_id' => (string)($item->expense_id ?? ''),
'unit_code' => (string)($item->unit_code ?? ''),
];
}
}
return [
'id' => $this->company->db.":".$this->id,
'name' => ctrans('texts.recurring_invoice') . " " . $this->number . " | " . $this->client->present()->name() . ' | ' . Number::formatMoney($this->amount, $this->company) . ' | ' . $this->translateDate($this->date, $this->company->date_format(), $locale),
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'is_deleted' => $this->is_deleted,
'number' => (string)$this->number,
'is_deleted' => (bool)$this->is_deleted,
'amount' => (float) $this->amount,
'balance' => (float) $this->balance,
'due_date' => $this->due_date,
@ -301,6 +340,7 @@ class RecurringInvoice extends BaseModel
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'po_number' => (string)$this->po_number,
'line_items' => $line_items,
];
}

View File

@ -109,6 +109,16 @@ class Task extends BaseModel
use Filterable;
use Searchable;
/**
* Get the index name for the model.
*
* @return string
*/
public function searchableAs(): string
{
return 'tasks_v2';
}
protected $fillable = [
'client_id',
'invoice_id',

View File

@ -171,9 +171,9 @@ class Vendor extends BaseModel
return [
'id' => $this->company->db.":".$this->id,
'name' => $name,
'is_deleted' => $this->is_deleted,
'is_deleted' => (bool)$this->is_deleted,
'hashed_id' => $this->hashed_id,
'number' => $this->number,
'number' => (string)$this->number,
'id_number' => $this->id_number,
'vat_number' => $this->vat_number,
'phone' => $this->phone,

View File

@ -126,20 +126,25 @@ class VendorContact extends Authenticatable implements HasLocalePreference
'send_email',
];
public function searchableAs(): string
{
return 'vendor_contacts_v2';
}
public function toSearchableArray()
{
return [
'id' => $this->company->db.":".$this->id,
'name' => $this->present()->search_display(),
'hashed_id' => $this->hashed_id,
'email' => $this->email,
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'phone' => $this->phone,
'custom_value1' => $this->custom_value1,
'custom_value2' => $this->custom_value2,
'custom_value3' => $this->custom_value3,
'custom_value4' => $this->custom_value4,
'email' => (string)$this->email,
'first_name' => (string)$this->first_name,
'last_name' => (string)$this->last_name,
'phone' => (string)$this->phone,
'custom_value1' => (string)$this->custom_value1,
'custom_value2' => (string)$this->custom_value2,
'custom_value3' => (string)$this->custom_value3,
'custom_value4' => (string)$this->custom_value4,
'company_key' => $this->company->company_key,
'vendor_id' => $this->vendor->hashed_id,
];

View File

@ -13,6 +13,10 @@ final class CreateRecurringInvoicesIndex implements MigrationInterface
*/
public function up(): void
{
// Force drop any existing indices to avoid mapping conflicts
Index::dropIfExists('recurring_invoices_v2');
Index::dropIfExists('recurring_invoices');
$mapping = [
'properties' => [
// Core recurring invoice fields

View File

@ -39,7 +39,6 @@ final class CreateVendorContactsIndex implements MigrationInterface
'company_key' => ['type' => 'keyword'],
'vendor_id' => ['type' => 'keyword'],
'send_email' => ['type' => 'boolean'],
'last_login' => ['type' => 'date'],
]
];