Linq различная запись, содержащая ключевые слова

Мне нужно вернуть отдельный список записей на основе поиска ключевых слов в автомобиле, например: "Alfa 147"

Проблема в том, что, поскольку у меня есть 3 автомобиля "Альфа", он возвращает 1 + 3 записи (кажется, 1 для результата Alfa и 147, а 3 для результата Alfa)

EDIT:

Запрос SQL-Server выглядит примерно так:

SELECT DISTINCT c.Id, c.Name /*, COUNT(Number of Ads in the KeywordAdCategories table with those 2 keywords) */
FROM Categories AS c
INNER JOIN KeywordAdCategories AS kac ON kac.Category_Id = c.Id
INNER JOIN KeywordAdCategories AS kac1 ON kac.Ad_Id = kac1.Ad_Id AND kac1.Keyword_Id = (SELECT Id FROM Keywords WHERE Name = 'ALFA')
INNER JOIN KeywordAdCategories AS kac2 ON kac1.Ad_Id = kac2.Ad_Id AND kac2.Keyword_Id = (SELECT Id FROM Keywords WHERE Name = '147')

Мой запрос LINQ:

       var query = from k in keywordQuery where splitKeywords.Contains(k.Name) 
                    join kac in keywordAdCategoryQuery on k.Id equals kac.Keyword_Id
                    join c in categoryQuery on kac.Category_Id equals c.Id
                    join a in adQuery on kac.Ad_Id equals a.Id
                    select new CategoryListByKeywordsDetailDto
                    {
                        Id = c.Id,
                        Name = c.Name,
                        SearchCount = keywordAdCategoryQuery.Where(s => s.Category_Id == c.Id).Where(s => s.Keyword_Id == k.Id).Distinct().Count(),
                        ListController = c.ListController,
                        ListAction = c.ListAction
                    };

        var searchResults = new CategoryListByBeywordsListDto();

        searchResults.CategoryListByKeywordsDetails = query.Distinct().ToList();

Объекты:

public class Keyword
{
    // Primary properties
    public int Id { get; set; }
    public string Name { get; set; }
}
// Keyword Sample Data:
// 1356 ALFA
// 1357 ROMEO
// 1358 145
// 1373 147

public class Category
{
    // Primary properties
    public int Id { get; set; }
    public string Name { get; set; }
}
// Category Sample Data
// 1    NULL    1   Carros
// 2    NULL    1   Motos
// 3    NULL    2   Oficinas
// 4    NULL    2   Stands
// 5    NULL    1   Comerciais
// 8    NULL    1   Barcos
// 9    NULL    1   Máquinas
// 10   NULL    1   Caravanas e Autocaravanas
// 11   NULL    1   Peças e Acessórios
// 12   1   1   Citadino
// 13   1   1   Utilitário
// 14   1   1   Monovolume

public class KeywordAdCategory
{
    [Key]
    [Column("Keyword_Id", Order = 0)]
    public int Keyword_Id { get; set; }

    [Key]
    [Column("Ad_Id", Order = 1)]
    public int Ad_Id { get; set; }

    [Key]
    [Column("Category_Id", Order = 2)]
    public int Category_Id { get; set; }
}
// KeywordAdCategory Sample Data
// 1356 1017    1
// 1356 1018    1
// 1356 1019    1
// 1357 1017    1
// 1357 1018    1
// 1357 1019    1
// 1358 1017    1
// 1373 1019    1

 public class Ad
{
    // Primary properties
    public int Id { get; set; }
    public string Title { get; set; }
    public string TitleStandard { get; set; }
    public string Version { get; set; }
    public int Year { get; set; }
    public decimal Price { get; set; }

    // Navigation properties
    public Member Member { get; set; }
    public Category Category { get; set; }
    public IList<Feature> Features { get; set; }
    public IList<Picture> Pictures { get; set; }
    public IList<Operation> Operations { get; set; }
}

public class AdCar : Ad
{
    public int Kms { get; set; }
    public Make Make { get; set; }
    public Model Model { get; set; }
    public Fuel Fuel { get; set; }
    public Color Color { get; set; }
}
// AdCar Sample Data
// 1017 Alfa Romeo 145 1.6TDI 2013  ALFA ROMEO 145 1.6TDI 2013  12  2       1.6TDI  1000    1   2013    1   20000,0000  2052    AdCar
// 1018 Alfa Romeo 146 1.6TDI 2013  ALFA ROMEO 146 1.6TDI 2013  12  2   5   1.6TDI  1000    2   2013    1   20000,0000  2052    AdCar
// 1019 Alfa Romeo 147 1.6TDI 2013  ALFA ROMEO  147 1.6TDI 2013 12  2   6   1.6TDI  1000    3   2013    1   20000,0000  2052    AdCar

В результате я ожидаю, что для поиска "ALFA" будет "Автомобили: 3", а для поиска "ALFA 147" - "Автомобили: 1", и на самом деле я получаю "Автомобили: 1\n Автомобили: 3"

Ответы

Ответ 1

Fiuu, это было крушение мозга. Я разложил запрос несколькими частями, но он был выполнен в целом в конце (результат var). И я вернул анонимный класс, но намерение понятное.

Вот решение:

var keywordIds = from k in keywordQuery
                    where splitKeywords.Contains(k.Name)
                    select k.Id;

var matchingKac = from kac in keywordAdCategories
            where keywordIds.Contains(kac.Keyword_Id)
            select kac;

var addIDs = from kac in matchingKac
                group kac by kac.Ad_Id into d
                where d.Count() == splitKeywords.Length
                select d.Key;

var groupedKac = from kac in keywordAdCategoryQuery
                where addIDs.Contains(kac.Ad_Id)
                group kac by new { kac.Category_Id, kac.Ad_Id };

var result = from grp in groupedKac
                group grp by grp.Key.Category_Id into final
                join c in categoryQuery on final.Key equals c.Id
                select new
                {
                    Id = final.Key,
                    Name = c.Name,
                    SearchCount = final.Count()
                };

// here goes result.ToList() or similar

Ответ 2

kac не фильтрует слова... так что это присоединение kac, kac1 и kac2 вернет 3 строки, так как это число ключевых слов для этого объявления

Вы должны удалить его.

Попробуйте следующее:

SELECT DISTINCT 
    c.Id, c.Name /*, COUNT(Number of Ads in the KeywordAdCategories table    with those 2 keywords) */
FROM 
    Categories AS c
INNER JOIN 
    KeywordAdCategories AS kac1 ON kac1.Keyword_Id = (SELECT Id 
                                                      FROM Keywords 
                                                      WHERE Name = 'ALFA')
                                AND kac1.Category_Id = c.Id
INNER JOIN 
    KeywordAdCategories AS kac2 ON kac1.Ad_Id = kac2.Ad_Id 
                                AND kac2.Keyword_Id = (SELECT Id 
                                                       FROM Keywords 
                                                       WHERE Name = '147')
                                AND kac2.Category_Id = c.Id

Я сделал тест...

Настройка эмбиента как

    declare @Keywords table(id int,name varchar(max))
    insert into @Keywords(id,name)
    values (1356,'ALFA')
    ,(1357,'ROMEO')
    ,(1358,'145')
    ,(1373,'147')

    declare @Categories table(id int, name varchar(max))
    insert into @Categories(id,name)
    values (1,'Carros')
    ,(2,'Motos')


    declare @KeywordAdCategories table(Keyword_Id int, ad_Id int,Category_Id int)
    insert into @KeywordAdCategories (Keyword_Id , ad_Id,Category_Id)
    values (1356, 1017,1)
    ,(1356, 1018,1)
    ,(1356, 1019,1)
    ,(1357, 1017,1)
    ,(1357, 1018,1)
    ,(1357, 1019,1)
    ,(1358, 1017,1)
    ,(1373, 1019,1)

Я запускаю эти два запроса:

--query 1
SELECT 
    c.Id, c.Name,COUNT(*) as [count]
FROM 
    @Categories AS c
INNER JOIN 
    @KeywordAdCategories AS kac1 ON kac1.Keyword_Id = (SELECT Id 
                                                       FROM @Keywords 
                                                       WHERE Name = 'ALFA')
                                 AND kac1.Category_Id = c.Id
GROUP BY 
    c.Id, c.Name

Я получаю этот набор результатов:

  Id          Name       count
  ----------- ---------- -----------
  1           Carros     3

и второй запрос для двух слов...

--query 2
SELECT 
    c.Id, c.Name,COUNT(*) as [count]
FROM 
    @Categories AS c
INNER JOIN 
    @KeywordAdCategories AS kac1 ON kac1.Keyword_Id = (SELECT Id 
                                                       FROM @Keywords 
                                                       WHERE Name = 'ALFA')
                                 AND kac1.Category_Id = c.Id
INNER JOIN 
    @KeywordAdCategories AS kac2 ON kac1.Ad_Id = kac2.Ad_Id 
                                 AND kac2.Keyword_Id = (SELECT Id 
                                                        FROM @Keywords 
                                                        WHERE Name = '147')
                                 AND kac2.Category_Id = c.Id
GROUP BY
    c.Id, c.Name

Набор результатов:

 Id          Name       count
 ----------- ---------- -----------
 1           Carros     1

Это то, что вы хотите?

Ответ 4

Разделите строку запроса в массив и выполните итерацию с помощью запроса к базе данных для каждого ключевого слова и объединения наборов результатов с помощью union. Результирующим набором будет каждая отдельная запись, соответствующая любому из указанных ключевых слов.

Ответ 5

Может, это близко? По крайней мере, подзапросы открывают для вас немного работы.

var query =
  from c in categoryQuery
  let keywords =
  (
    from k in keywordQuery where splitKeywords.Contains(k.Name)
    join kac in keywordAdCategoryQuery on k.Id equals kac.Keyword_Id
    where kac.Category_Id == c.Id
    join a in adQuery on kac.Ad_Id equals a.Id
    select k.Id
  ).Distinct()
  where keywords.Any()
  select new CategoryListByKeywordsDetailDto
  {
    Id = c.Id,
    Name = c.Name,
    SearchCount =
    (
      from kac in keywordAdCategoryQuery
      where kac.Category_Id == c.Id
      join kId in keywords on kac.Keyword_Id equals kId
      select kac.Id
    ).Distinct().Count(),
    ListController = c.ListController,
    ListAction = c.ListAction
  };

Ответ 6

Итак, если я правильно понимаю необходимость, вы хотите, чтобы все подмножество слов соответствовало тексту, а не совпадению OR, которое вы сейчас получаете? Я вижу по крайней мере два параметра, первый из которых не может перевести split на SQL:

var query = from k in keywordQuery where !splitKeywords.Except(k.Name.split(' ')).Any()

Это делает следующие предположения:

  • Ваши слова в ключевых словах ограничены пробелом.
  • Вы ищете точные совпадения, а не частичные совпадения. (I.e. Test не будет соответствовать TestTest).

Другой вариант заключается в том, чтобы динамически генерировать предикат с использованием предиката-строителя (не делал этого через некоторое время, моя реализация может потребоваться настройка), но это более вероятно (и лучше на мой взгляд) решение):

var predicate = PredicateBuilder.True<keywordQuery>();
foreach (string s in splitKeywords) {
    predicate.AND(s.Contains(k.Name));
}

query.Where(predicate);

Если кто-то может прокомментировать, если какой-то из моих синтаксисов выключен, я был бы признателен. EDIT: включение ссылки на хорошую ссылку на построитель предикатов: http://www.albahari.com/nutshell/predicatebuilder.aspx

UPDATE

Создатель Predicate для нескольких таблиц, если кто-то ищет здесь, как это сделать. Может ли PredicateBuilder генерировать предикаты, охватывающие несколько таблиц?

Ответ 7

Одна из красивейших особенностей linq заключается в том, что вы можете создавать сложные запросы в более мелких и простых шагах и позволить linq выяснить, как объединить их все вместе.

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

Предполагая, что ключевые слова определены как

var keywords = "Alfa 147";
var splitKeywords = keywords.Split(new char[] {' '});

Этап 1

Получить список ключевых слов, сгруппированных по объявлениям и категориям, и

var subQuery = (from kac in keywordAdCategoryQuery  
    join k in keywordQuery  on kac.Keyword_Id equals k.Id 
    select new 
    {        
        kac.Ad_Id, 
        kac.Category_Id, 
        KeyWord = k.Name, 
    }); 

var grouped = (from r in subQuery 
    group r by new { r.Ad_Id, r.Category_Id}  into results
    select new 
    { 
        results.Key.Ad_Id , 
        results.Key.Category_Id , 
        keywords = (from r in results select r.KeyWord) 
    });

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

Этап 2

Отфильтровать любые группы, у которых нет каждого из ключевых слов

foreach(var keyword in splitKeywords)
{
    var copyOfKeyword = keyword ;   // Take copy of keyword to avoid closing over loop
    grouped = (from r in grouped where r.keywords.Contains(copyOfKeyword) select r) ;
}

Этап 3

Группировать по категориям и подсчитывать результаты для каждой категории

var groupedByCategories = (from r in grouped 
    group r by r.Category_Id into results 
    join c in categoryQuery  on results.Key equals c.Id 
    select new 
    { 
        c.Id , 
        c.Name , 
        Count = results.Count()
    });

Этап 4

Теперь извлеките информацию из sql. Это нужно сделать в одном запросе.

var finalResults = groupedByCategories.ToList();

Ответ 8

Должно быть возможным запросить каждое ключевое слово, а затем объединить результирующие множества. Дублирующие значения будут удалены из объединения, и вы сможете выработать необходимые скопления.

Ответ 9

Попробуйте удалить класс, выбрав

 var query = (from k in keywordQuery where splitKeywords.Contains(k.Name) 
                        join kac in keywordAdCategoryQuery on k.Id equals kac.Keyword_Id
                        join c in categoryQuery on kac.Category_Id equals c.Id
                        join a in adQuery on kac.Ad_Id equals a.Id
                        select new
                        {
                            Id = c.Id,
                            Name = c.Name,
                            SearchCount = keywordAdCategoryQuery.Where(s => s.Category_Id == c.Id).Where(s => s.Keyword_Id == k.Id).Distinct().Count(),
                            ListController = c.ListController,
                            ListAction = c.ListAction
                        }).Distinct().ToList();

        var searchResults = new CategoryListByBeywordsListDto();



searchResults.CategoryListByKeywordsDetails = (from q in query select new           CategoryListByKeywordsDetailDto
{
                        Id = q.Id,
                        Name = q.Name,
                        SearchCount = q.SearchCount,
                        ListController = q.ListController,
                        ListAction = q.ListAction
                    }).ToList();

Ответ 10

привет, если я правильно понимаю вашу проблему

"Проблема в том, что, поскольку у меня есть 3 автомобиля" Альфа ", он возвращает 1 + 3 (кажется 1 для Альфы и 147 результатов, а 3 для Альфы результат)"

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

    public Linqfilter()
    {
        //as Note: I modified a few classes from you because i doesn'T have your Member, Operation, Make,... classes

        #region declaration
        var originalAdCarList = new List<AdCar>() 
        {
            new AdCar(){Id=1017, Title= "Alfa Romeo 145 1.6TDI 2013", Category= new Category(){Id =12}} ,
            new AdCar(){Id=1018, Title= "Alfa Romeo 146 1.6TDI 2013", Category= new Category(){Id =11}} ,
            new AdCar(){Id=1019, Title= "Alfa Romeo 147 1.6TDI 2013", Category= new Category(){Id =12}} 
        };

        var originalKeywordAdCategoryList = new List<KeywordAdCategory>() 
        {
            new KeywordAdCategory() { Keyword_Id=1356, Ad_Id=1017,Category_Id=1},
            new KeywordAdCategory() { Keyword_Id=1356, Ad_Id=1018,Category_Id=1},
            new KeywordAdCategory() { Keyword_Id=1356, Ad_Id=1019,Category_Id=1},
            new KeywordAdCategory() { Keyword_Id=1357, Ad_Id=1017,Category_Id=1},
            new KeywordAdCategory() { Keyword_Id=1357, Ad_Id=1018,Category_Id=1},
            new KeywordAdCategory() { Keyword_Id=1357, Ad_Id=1019,Category_Id=1},
            new KeywordAdCategory() { Keyword_Id=1358, Ad_Id=1017,Category_Id=1},
            new KeywordAdCategory() { Keyword_Id=1373, Ad_Id=1019,Category_Id=1}            
        };

        var originalCategoryList = new List<Category>()
        {
            new Category(){Id=1,    Name="NULL    1   Carros"},
            new Category(){Id=2,    Name="NULL    1   Motos"},
            new Category(){Id=3,    Name="NULL    2   Oficinas"},
            new Category(){Id=4 ,   Name="NULL    2   Stands"},
            new Category(){Id=5 ,   Name="NULL    1   Comerciais"},
            new Category(){Id=8,    Name="NULL    1   Barcos"},
            new Category(){Id=9 ,   Name="NULL    1   Máquinas"},
            new Category(){Id=10 ,  Name="NULL    1   Caravanas e Autocaravanas"},
            new Category(){Id=11 ,  Name="NULL    1   Peças e Acessórios"},
            new Category(){Id=12 ,  Name="1   1   Citadino"},
            new Category(){Id=13 ,  Name="1   1   Utilitário"},
            new Category(){Id=14 ,  Name="1   1   Monovolume"}
        };


        var originalKeywordList = new List<Keyword>() 
        {
             new Keyword(){Id=1356 ,Name="ALFA"},
             new Keyword(){Id=1357 ,Name="ROMEO"},
             new Keyword(){Id=1358 ,Name="145"},
             new Keyword(){Id=1373 ,Name="147"}
        };
        #endregion declaration

        string searchText = "ALFA";

        // split the string searchText in an Array of substrings
        var splitSearch = searchText.Split(' '); 


        var searchKeyList =new List<Keyword>();

        // generate a list of Keyword based on splitSearch
        foreach (string part in splitSearch)
            if(originalKeywordList.Any(key => key.Name == part))
                searchKeyList.Add(originalKeywordList.First(key => key.Name == part));

        // generate a list of KeywordAdCategory  based on searchKList
        var searchKACList = new List<KeywordAdCategory>();
        foreach(Keyword key in searchKeyList)
            foreach (KeywordAdCategory kAC in originalKeywordAdCategoryList.Where(kac => kac.Keyword_Id == key.Id))
                searchKACList.Add(kAC);


        var groupedsearchKAClist = from kac in searchKACList group kac by kac.Keyword_Id;

        var listFiltered = new List<AdCar>(originalAdCarList);

        //here starts the real search part
        foreach (IGrouping<int, KeywordAdCategory> kacGroup in groupedsearchKAClist)
        {

            var listSingleFiltered = new List<AdCar>();
            //  generate a list of AdCar that matched the current KeywordAdCategory filter
            foreach (KeywordAdCategory kac in kacGroup)
                foreach (AdCar aCar in originalAdCarList.Where(car => car.Id == kac.Ad_Id))
                    listSingleFiltered.Add(aCar);

            var tempList = new List<AdCar>(listFiltered);
            // iterrates over a temporary copie of listFiltered and removes items which don't match to the current listSingleFiltered
            foreach (AdCar aC in tempList)
                if (!listSingleFiltered.Any(car => car.Id == aC.Id))
                    listFiltered.Remove(aC);
        }

        var AdCarCount = listFiltered.Count; // is the count of the AdCar who match

        var CatDic =new  Dictionary<Category, int>(); // will contain the Counts foreach Categorie > 0

        foreach(AdCar aCar in listFiltered)
            if(originalCategoryList.Any(cat => cat.Id ==aCar.Category.Id))
            {
                var selectedCat = originalCategoryList.First(cat => cat.Id == aCar.Category.Id);
                if (!CatDic.ContainsKey(selectedCat))
                {
                    CatDic.Add(selectedCat, 1);//new Category Countvalue
                }
                else
                {
                    CatDic[selectedCat]++; //Category Countvalue +1
                }
            }
    }
}

public class Keyword
{
    // Primary properties
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Category
{
    // Primary properties
    public int Id { get; set; }
    public string Name { get; set; }
}

public class KeywordAdCategory
{
    //[Key]
    //[Column("Keyword_Id", Order = 0)]
    public int Keyword_Id { get; set; }

    //[Key]
    //[Column("Ad_Id", Order = 1)]
    public int Ad_Id { get; set; }

    //[Key]
    //[Column("Category_Id", Order = 2)]
    public int Category_Id { get; set; }
}

public class Ad
{
    // Primary properties
    public int Id { get; set; }
    public string Title { get; set; }
    public string TitleStandard { get; set; }
    public string Version { get; set; }
    public int Year { get; set; }
    public decimal Price { get; set; }

    // Navigation properties
    public string Member { get; set; }
    public Category Category { get; set; }
    public IList<string> Features { get; set; }
    public IList<int> Pictures { get; set; }
    public IList<string> Operations { get; set; }
}

public class AdCar : Ad
{
    public int Kms { get; set; }
    public string Make { get; set; }
    public int Model { get; set; }
    public int Fuel { get; set; }
    public int Color { get; set; }
}

надеюсь, это поможет вам или кому-то еще

Изменить:

расширил мой метод Linqfilter(), чтобы ответить на запрос

Edit2:

Я думаю, это должно быть именно то, что вы ищете

        var selectedKWLinq = from kw in originalKeywordList
                             where splitSearch.Contains(kw.Name) 
                             select kw;

        var selectedKACLinq = from kac in originalKeywordAdCategoryList
                              where selectedKWLinq.Any<Keyword>(item => item.Id == kac.Keyword_Id) 
                              group kac by kac.Keyword_Id into selectedKAC
                              select selectedKAC;

        var selectedAdCar = from adC in originalAdCarList
                           where (from skAC in selectedKACLinq
                                      where skAC.Any(kac => kac.Ad_Id == adC.Id)
                                  select skAC).Count() == selectedKACLinq.Count()
                           select adC;


        var selectedCategorys = from cat in originalCategoryList
                                join item in selectedAdCar
                                on cat.Id equals item.Category.Id
                                group cat by cat.Id into g
                                select g;

        //result part
        var AdCarCount = selectedAdCar.Count(); 

        List<IGrouping<int, Category>> list = selectedCategorys.ToList(); 
        var firstCategoryCount = list[0].Count();
        var secoundCategoryCount = list[1].Count();

Ответ 11

Вы делаете выделение в списке CategoryListByKeywordsDetailDto. Отдельный работает только с POCO и анонимными объектами. В вашем случае вам нужно реализовать IEqualitycomparer для выбора отличного для работы.

Ответ 12

Я пробовал это с помощью LINQ напрямую в коллекциях памяти (как, например, в SQL) - кажется, работает для меня (я считаю, что главное, что вы хотите искать объявления, которые применяются ко всем указанным ключевым словам, а не ЛЮБОЙ, правильный? Во всяком случае, некоторый пример кода ниже (небольшой комментарий-иш и не обязательно самый эффективный, но, надеюсь, иллюстрирует точку...)

Работа со следующими "наборами данных":

private List<AdCar> AdCars = new List<AdCar>();
private List<KeywordAdCategory> KeywordAdCategories = new List<KeywordAdCategory>();
private List<Category> Categories = new List<Category>();
private List<Keyword> Keywords = new List<Keyword>();

которые заполняются в тестовом методе, используя предоставленные вами данные...

Метод поиска выглядит примерно так:

var splitKeywords = keywords.Split(' ');

var validKeywords = Keywords.Join(splitKeywords, kwd => kwd.Name.ToLower(), spl => spl.ToLower(), (kwd, spl) => kwd.Id).ToList();

var groupedAdIds = KeywordAdCategories
                .GroupBy(kac => kac.Ad_Id)
                .Where(grp => validKeywords.Except(grp.Select(kac => kac.Keyword_Id)).Any() == false)
                .Select(grp => grp.Key)
                .ToList();

var foundKacs = KeywordAdCategories
    .Where(kac => groupedAdIds.Contains(kac.Ad_Id))
    .GroupBy(kbc => kbc.Category_Id, kac => kac.Ad_Id);

//Results count by category
var catCounts = Categories
    .Join(foundKacs, cat => cat.Id, kacGrp => kacGrp.Key, (cat, kacGrp) => new { CategoryName = cat.Name, AdCount = kacGrp.Distinct().Count() })
    .ToList();

//Actual results set
var ads = AdCars.Join(groupedAdIds, ad => ad.Id, grpAdId => grpAdId, (ad, grpAdId) => ad);

Как я уже сказал, это больше для иллюстрации, пожалуйста, не смотрите слишком внимательно на использование Joins и GroupBy и т.д. (не уверен, что это точно, er, "оптимально" )

Итак, используя вышеизложенное, если я ищу "Альфа", я получаю 3 объявления, и если я ищу "Alfa 147", я получаю всего 1 результат.

EDIT: я изменил код, чтобы представить два возможных результата (поскольку я не был уверен, что было необходимо по вашему вопросу)

ads предоставит вам фактические Объявления, возвращенные поиском

catCounts предоставит список анонимных типов, каждый из которых будет отображать результаты поиска в виде количества объявлений по категориям

Помогает ли это?