Сортировка по релевантности с помощью коллекций Laravel whereHas()

#php #laravel #eloquent

Вопрос:

Возьмите этот рабочий код. Сначала он выполняет поиск в users таблице по ключевому слову, а затем использует whereHas() метод поиска в сводной таблице category_user для любых пользователей, отнесенных к данной категории на основе поиска. Это работает хорошо, все записи извлекаются.

Итак, чтобы быть абсолютно ясным, используются 3 таблицы, users , categories и category_user .

Я использую коллекции Laravel sortByDesc() для упорядочивания записей на основе релевантности. Итак, если категория 1, категория 2 и ключевое слово из моего примера поискового запроса были сохранены для пользователя 1, и только категория 1 была сохранена для пользователя 2, пользователь 1 будет отображаться первым в результатах.

Приведенный ниже код делает именно это, но ТОЛЬКО для users таблицы. У меня было хорошее сканирование, и я не могу найти способ применить это решение к этому типу запросов из-за уникальности способа, которым мне нужно заказать. Кто-нибудь может посоветовать какие-либо методы?

 //props for sortByDesc
$props = ['name', 'slug', 'location', 'company_name', 'biography', 'usually_active'];

//Search Query
//1. Search users table for keywords
//2. use whereHas() to search pivot table of categories for keyword and filters selected
//3. use sortByDesc collections to order results based number of matches, most matches ordered at the top
$search_results = User::where('approved', '=', 1)
->where('user_type_id', '=', 1)
->orWhere(function ($q) use ($search_items) {        
    
    //search users tables
    foreach ($search_items as $value) {
        $q->orWhere('name', 'like', "%{$value}%");
        $q->orWhere('location', 'like', "%{$value}%");
        $q->orWhere('company_name', 'like', "%{$value}%");
        $q->orWhere('biography', 'like', "%{$value}%");
        $q->orWhere('usually_active', 'like', "%{$value}%");
    }

})->whereHas('categories', function ($q) use($search_items) {
    
    //search categories table
    if(!empty($search_items)):
        $q->whereIn('name', $search_items);
        $q->orWhereIn('slug', $search_items);
    endif;

//Now we sort based on best matches
})->get()->sortByDesc(function($i, $k) use ($search_items, $props) {
    
    // The bigger the weight, the higher the record
    $weight = 0;

    // Iterate through search terms
    foreach($search_items as $item) {
        foreach($props as $prop) { 
            if(strpos($i->{$prop}, $item) !== false)
                $weight  = 1; // Increase weight if the search term is found
        }
    }

    return $weight;
});

//get the results and paginate
$search_results = $search_results->values()->all();
$search_results = $this->paginate($search_results);
 

Ответ №1:

Короткий ответ:

Добавьте select для столбцов категорий, которые вы хотите отфильтровать. Будьте осторожны, результирующий набор данных будет содержать столбцы типа categories.name so, поэтому вы можете соответствующим образом настроить ключи search_items или псевдоним added select.

Для этого вам нужно присоединиться к таблице categories в запросе, я угадал ваши внешние ключи, но не стесняйтесь исправлять их в предложении join, чтобы они соответствовали тому, что у вас есть в базе данных.

Таким образом, вы можете упростить свои предложения where только в одном

 <?
User::join('categories', 'user.categories_id', '=', 'categories.id')
where('approved', '=', 1)
->where('user_type_id', '=', 1)
->orWhere(function ($q) use ($search_items) {        
    
    //search users tables
    foreach ($search_items as $value) {
        $q->orWhere('name', 'like', "%{$value}%");
        $q->orWhere('location', 'like', "%{$value}%");
        $q->orWhere('company_name', 'like', "%{$value}%");
        $q->orWhere('biography', 'like', "%{$value}%");
        $q->orWhere('usually_active', 'like', "%{$value}%");
        $q->orWhereIn('categories.name', $search_items);
        $q->orWhereIn('categories.slug', $search_items);
    }

})
// Add the columns you want to sort on
->addSelect(['categories.name', 'categories.slug']) 
->get()->sortByDesc(function($i, $k) use ($search_items, $props) {
    
    // The bigger the weight, the higher the record
    $weight = 0;

    // Iterate through search terms
    foreach($search_items as $item) {
        foreach($props as $prop) { 
            if(strpos($i->{$prop}, $item) !== false)
                $weight  = 1; // Increase weight if the search term is found
        }
    }

    return $weight;
});
 

Более длинный ответ:

Когда вы запускаете свой запрос из модели, в вашем случае User , eloquent автоматически добавляет столбцы этой модели в select, независимо от отношений, которые определены в модели.

Если вы хотите получить доступ к столбцам в объединенных таблицах, вам нужно вручную добавить их в select with ->addSelect([...]) .

Вы можете прочитать больше об этом в официальных документах: https://laravel.com/docs/8.x/queries#specifying-a-select-clause

Комментарии:

1. Спасибо, что нашли время ответить. Я чувствую, что это на правильном пути, когда я добавляю в -> addSelect([‘categories.name ‘, ‘categories.slug’]) он пытается вытащить categories.name и категории. извлекается из таблицы users, а не из таблицы categories. Есть ли что-нибудь дополнительное, что требуется для того, чтобы эта оценка работала?

2. О, может быть, вам нужно добавить соединение в таблицу категорий, тогда я отредактирую свой ответ

3. Я добавил объединение и упростил предложение where, поскольку поля категорий теперь доступны в запросе

4. Нужно ли объединять сводную таблицу вместо или вместе с категориями? поскольку таблица categories ничего не хранит для пользователя, сводная таблица category_user хранит.

5. О, есть точка опоры? Итак, у вас много категорий на пользователя? В этом случае, каков именно предполагаемый результат в итоге? Одна строка на пользователя или одна строка на категорию? Возможно, это могло бы быть более понятным для схемы базы данных и предполагаемого примера результата данных

Ответ №2:

Решение состояло в том, чтобы использовать withCount() метод для определения количества категорий, сохраненных для пользователя в сводной таблице. Затем я мог бы использовать categories_count returned с каждым элементом для упорядочивания записей на основе количества помеченных категорий на пользователя.

 $search_results = User::where('approved', '=', 1)
    ->where('user_type_id', '=', 1)
    ->where(function ($q) use ($search_items) {        
    
        //search users tables
        foreach ($search_items as $value) {
            $q->orWhere('users.name', 'like', "%{$value}%");
            $q->orWhere('users.location', 'like', "%{$value}%");
            $q->orWhere('users.company_name', 'like', "%{$value}%");
            $q->orWhere('users.biography', 'like', "%{$value}%");
        }

    })->orWhereHas('categories', function ($q) use($search_items) {

        if(!empty($search_items)):
            $q->whereIn('name', $search_items);
            $q->orWhereIn('slug', $search_items);
        endif;

    //Now we sort based on best matches
    })->withCount([
        'categories' => function ($q) use($search_items) {
            
            //search categories table
            if(!empty($search_items)):
                $q->whereIn('name', $search_items);
                $q->orWhereIn('slug', $search_items);
            endif;
        }

    ])->orderByDesc('categories_count')->paginate(25);